Move caching tests into a dedicated class

plus some tests refactorings
This commit is contained in:
Ilya Chernikov
2019-07-24 12:02:30 +02:00
parent ef23033936
commit c7eb9e9520
2 changed files with 226 additions and 201 deletions
@@ -0,0 +1,205 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.script.experimental.jvmhost.test
import junit.framework.TestCase
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Test
import java.io.File
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.nio.file.Files
import java.security.MessageDigest
import kotlin.script.experimental.api.*
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.BasicJvmScriptEvaluator
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
import kotlin.script.experimental.jvm.impl.KJvmCompiledScript
import kotlin.script.experimental.jvmhost.*
import kotlin.script.templates.standard.SimpleScriptTemplate
class CachingTest : TestCase() {
val simpleScript = "val x = 1\nprintln(\"x = \$x\")"
val simpleScriptExpectedOutput = listOf("x = 1")
val scriptWithImport = "println(\"Hello from imported \$helloScriptName script!\")"
val scriptWithImportExpectedOutput = listOf("Hello from helloWithVal script!", "Hello from imported helloWithVal script!")
@Test
fun testMemoryCache() {
val cache = SimpleMemoryScriptsCache()
checkWithCache(cache, simpleScript, simpleScriptExpectedOutput)
}
@Test
fun testSimpleImportWithMemoryCache() {
val cache = SimpleMemoryScriptsCache()
checkWithCache(cache, scriptWithImport, scriptWithImportExpectedOutput) { makeSimpleConfigurationWithTestImport() }
}
@Test
fun testFileCache() {
withTempDir("scriptingTestCache") { cacheDir ->
val cache = FileBasedScriptCache(cacheDir)
Assert.assertTrue(cache.baseDir.listFiles().isEmpty())
checkWithCache(cache, simpleScript, simpleScriptExpectedOutput)
}
}
@Test
fun testSimpleImportWithFileCache() {
withTempDir("scriptingTestCache") { cacheDir ->
val cache = FileBasedScriptCache(cacheDir)
Assert.assertTrue(cache.baseDir.listFiles().isEmpty())
checkWithCache(cache, scriptWithImport, scriptWithImportExpectedOutput) { makeSimpleConfigurationWithTestImport() }
}
}
private fun checkWithCache(
cache: ScriptingCacheWithCounters, script: String, expectedOutput: List<String>,
configurationBuilder: ScriptCompilationConfiguration.Builder.() -> Unit = {}
) {
val compiler = JvmScriptCompiler(defaultJvmScriptingHostConfiguration, cache = cache)
val evaluator = BasicJvmScriptEvaluator()
val host = BasicJvmScriptingHost(compiler = compiler, evaluator = evaluator)
val scriptCompilationConfiguration =
createJvmCompilationConfigurationFromTemplate<SimpleScriptTemplate>(body = configurationBuilder)
Assert.assertEquals(0, cache.storedScripts)
var compiledScript: CompiledScript<*>? = null
val output = captureOut {
runBlocking {
compiler(script.toScriptSource(), scriptCompilationConfiguration).onSuccess {
compiledScript = it
evaluator(it)
}.throwOnFailure()
}
}.lines()
Assert.assertEquals(expectedOutput, output)
Assert.assertEquals(1, cache.storedScripts)
Assert.assertEquals(0, cache.retrievedScripts)
val cachedScript = cache.get(script.toScriptSource(), scriptCompilationConfiguration)
Assert.assertNotNull(cachedScript)
Assert.assertEquals(1, cache.retrievedScripts)
val compiledScriptClassRes = runBlocking { compiledScript!!.getClass(null) }
val cachedScriptClassRes = runBlocking { cachedScript!!.getClass(null) }
val compiledScriptClass = compiledScriptClassRes.valueOrNull()
val cachedScriptClass = cachedScriptClassRes.valueOrNull()
Assert.assertEquals(compiledScriptClass!!.qualifiedName, cachedScriptClass!!.qualifiedName)
Assert.assertEquals(compiledScriptClass!!.supertypes, cachedScriptClass!!.supertypes)
val output2 = captureOut {
runBlocking {
evaluator(cachedScript!!).throwOnFailure()
}
}.lines()
Assert.assertEquals(output, output2)
val output3 = captureOut { evalScriptWithConfiguration(script, host, configurationBuilder).throwOnFailure() }.lines()
Assert.assertEquals(2, cache.retrievedScripts)
Assert.assertEquals(output, output3)
}
}
private interface ScriptingCacheWithCounters : CompiledJvmScriptsCache {
val storedScripts: Int
val retrievedScripts: Int
}
private class SimpleMemoryScriptsCache : ScriptingCacheWithCounters {
internal val data = hashMapOf<Pair<SourceCode, ScriptCompilationConfiguration>, CompiledScript<*>>()
private var _storedScripts = 0
private var _retrievedScripts = 0
override val storedScripts: Int
get() = _storedScripts
override val retrievedScripts: Int
get() = _retrievedScripts
override fun get(script: SourceCode, scriptCompilationConfiguration: ScriptCompilationConfiguration): CompiledScript<*>? =
data[script to scriptCompilationConfiguration]?.also { _retrievedScripts++ }
override fun store(
compiledScript: CompiledScript<*>,
script: SourceCode,
scriptCompilationConfiguration: ScriptCompilationConfiguration
) {
data[script to scriptCompilationConfiguration] = compiledScript
_storedScripts++
}
}
private class FileBasedScriptCache(val baseDir: File) : ScriptingCacheWithCounters {
override fun get(script: SourceCode, scriptCompilationConfiguration: ScriptCompilationConfiguration): CompiledScript<*>? {
val file = File(baseDir, uniqueScriptHash(script, scriptCompilationConfiguration))
return if (!file.exists()) null else file.readCompiledScript(scriptCompilationConfiguration)?.also { retrievedScripts++ }
}
override fun store(
compiledScript: CompiledScript<*>,
script: SourceCode,
scriptCompilationConfiguration: ScriptCompilationConfiguration
) {
val file = File(baseDir, uniqueScriptHash(script, scriptCompilationConfiguration))
file.outputStream().use { fs ->
ObjectOutputStream(fs).use { os ->
os.writeObject(compiledScript)
}
}
storedScripts++
}
override var storedScripts: Int = 0
private set
override var retrievedScripts: Int = 0
private set
}
internal fun uniqueScriptHash(script: SourceCode, scriptCompilationConfiguration: ScriptCompilationConfiguration): String {
val digestWrapper = MessageDigest.getInstance("MD5")
digestWrapper.update(script.text.toByteArray())
scriptCompilationConfiguration.entries().sortedBy { it.key.name }.forEach {
digestWrapper.update(it.key.name.toByteArray())
digestWrapper.update(it.value.toString().toByteArray())
}
return digestWrapper.digest().toHexString()
}
private fun File.readCompiledScript(scriptCompilationConfiguration: ScriptCompilationConfiguration): CompiledScript<*> {
return inputStream().use { fs ->
ObjectInputStream(fs).use {
it.readObject() as KJvmCompiledScript<*>
}
}
}
private fun ByteArray.toHexString(): String = joinToString("", transform = { "%02x".format(it) })
private fun <R> withTempDir(keyName: String = "tmp", body: (File) -> R) {
val tempDir = Files.createTempDirectory(keyName).toFile()
try {
body(tempDir)
} finally {
tempDir.deleteRecursively()
}
}
@@ -18,7 +18,6 @@ import java.io.*
import java.lang.RuntimeException
import java.net.URLClassLoader
import java.nio.file.Files
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
import java.util.jar.JarFile
import kotlin.reflect.KClass
@@ -26,7 +25,6 @@ import kotlin.script.experimental.api.*
import kotlin.script.experimental.host.BasicScriptingHost
import kotlin.script.experimental.host.FileBasedScriptSource
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.BasicJvmScriptEvaluator
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
import kotlin.script.experimental.jvm.impl.KJvmCompiledScript
import kotlin.script.experimental.jvm.updateClasspath
@@ -162,21 +160,7 @@ class ScriptingHostTest : TestCase() {
checkInvokeMain(null) // isolated
checkInvokeMain(Thread.currentThread().contextClassLoader)
val javaExecutable = File(File(System.getProperty("java.home"), "bin"), "java")
val args = listOf(javaExecutable.absolutePath, "-jar", outJar.path)
val processBuilder = ProcessBuilder(args)
processBuilder.redirectErrorStream(true)
val outputFromProcess = run {
val process = processBuilder.start()
process.waitFor(10, TimeUnit.SECONDS)
val out = process.inputStream.reader().readText()
if (process.isAlive) {
process.destroyForcibly()
"Error: timeout, killing script process\n$out"
} else {
out
}
}.trim()
val outputFromProcess = runScriptFromJar(outJar)
Assert.assertEquals(greeting, outputFromProcess)
}
@@ -356,103 +340,6 @@ class ScriptingHostTest : TestCase() {
jvmTargetTestImpl("9", 53)
}
@Test
fun testMemoryCache() {
val script = "val x = 1\nprintln(\"x = \$x\")"
val expectedOutput = listOf("x = 1")
val cache = SimpleMemoryScriptsCache()
checkWithCache(cache, script, expectedOutput)
}
@Test
fun testSimpleImportWithMemoryCache() {
val script = "println(\"Hello from imported \$helloScriptName script!\")"
val expectedOutput = listOf("Hello from helloWithVal script!", "Hello from imported helloWithVal script!")
val cache = SimpleMemoryScriptsCache()
checkWithCache(cache, script, expectedOutput) { makeSimpleConfigurationWithTestImport() }
}
@Test
fun testFileCache() {
val script = "val x = 1\nprintln(\"x = \$x\")"
val expectedOutput = listOf("x = 1")
val cacheDir = Files.createTempDirectory("scriptingTestCache").toFile()
try {
val cache = FileBasedScriptCache(cacheDir)
Assert.assertTrue(cache.baseDir.listFiles().isEmpty())
checkWithCache(cache, script, expectedOutput)
} finally {
cacheDir.deleteRecursively()
}
}
@Test
fun testSimpleImportWithFileCache() {
val script = "println(\"Hello from imported \$helloScriptName script!\")"
val expectedOutput = listOf("Hello from helloWithVal script!", "Hello from imported helloWithVal script!")
val cacheDir = Files.createTempDirectory("scriptingTestCache").toFile()
try {
val cache = FileBasedScriptCache(cacheDir)
Assert.assertTrue(cache.baseDir.listFiles().isEmpty())
checkWithCache(cache, script, expectedOutput) { makeSimpleConfigurationWithTestImport() }
} finally {
cacheDir.deleteRecursively()
}
}
private fun checkWithCache(
cache: ScriptingCacheWithCounters, script: String, expectedOutput: List<String>,
configurationBuilder: ScriptCompilationConfiguration.Builder.() -> Unit = {}
) {
val compiler = JvmScriptCompiler(defaultJvmScriptingHostConfiguration, cache = cache)
val evaluator = BasicJvmScriptEvaluator()
val host = BasicJvmScriptingHost(compiler = compiler, evaluator = evaluator)
val scriptCompilationConfiguration =
createJvmCompilationConfigurationFromTemplate<SimpleScriptTemplate>(body = configurationBuilder)
Assert.assertEquals(0, cache.storedScripts)
var compiledScript: CompiledScript<*>? = null
val output = captureOut {
runBlocking {
compiler(script.toScriptSource(), scriptCompilationConfiguration).onSuccess {
compiledScript = it
evaluator(it)
}.throwOnFailure()
}
}.lines()
Assert.assertEquals(expectedOutput, output)
Assert.assertEquals(1, cache.storedScripts)
Assert.assertEquals(0, cache.retrievedScripts)
val cachedScript = cache.get(script.toScriptSource(), scriptCompilationConfiguration)
Assert.assertNotNull(cachedScript)
Assert.assertEquals(1, cache.retrievedScripts)
val compiledScriptClassRes = runBlocking { compiledScript!!.getClass(null) }
val cachedScriptClassRes = runBlocking { cachedScript!!.getClass(null) }
val compiledScriptClass = compiledScriptClassRes.valueOrNull()
val cachedScriptClass = cachedScriptClassRes.valueOrNull()
Assert.assertEquals(compiledScriptClass!!.qualifiedName, cachedScriptClass!!.qualifiedName)
Assert.assertEquals(compiledScriptClass!!.supertypes, cachedScriptClass!!.supertypes)
val output2 = captureOut {
runBlocking {
evaluator(cachedScript!!).throwOnFailure()
}
}.lines()
Assert.assertEquals(output, output2)
val output3 = captureOut { evalScriptWithConfiguration(script, host, configurationBuilder).throwOnFailure() }.lines()
Assert.assertEquals(2, cache.retrievedScripts)
Assert.assertEquals(output, output3)
}
@Test
fun testCompiledScriptClassLoader() {
val script = "val x = 1"
@@ -486,6 +373,24 @@ class ScriptingHostTest : TestCase() {
}
}
internal fun runScriptFromJar(jar: File): String {
val javaExecutable = File(File(System.getProperty("java.home"), "bin"), "java")
val args = listOf(javaExecutable.absolutePath, "-jar", jar.path)
val processBuilder = ProcessBuilder(args)
processBuilder.redirectErrorStream(true)
return run {
val process = processBuilder.start()
process.waitFor(10, TimeUnit.SECONDS)
val out = process.inputStream.reader().readText()
if (process.isAlive) {
process.destroyForcibly()
"Error: timeout, killing script process\n$out"
} else {
out
}
}.trim()
}
fun <T> ResultWithDiagnostics<T>.throwOnFailure(): ResultWithDiagnostics<T> = apply {
if (this is ResultWithDiagnostics.Failure) {
val firstExceptionFromReports = reports.find { it.exception != null }?.exception
@@ -506,7 +411,7 @@ private fun evalScriptWithResult(
): ResultValue =
evalScriptWithConfiguration(script, host, body).throwOnFailure().valueOrNull()!!.returnValue
private fun evalScriptWithConfiguration(
internal fun evalScriptWithConfiguration(
script: String,
host: BasicScriptingHost = BasicJvmScriptingHost(),
body: ScriptCompilationConfiguration.Builder.() -> Unit = {}
@@ -515,40 +420,7 @@ private fun evalScriptWithConfiguration(
return host.eval(script.toScriptSource(), compilationConfiguration, null)
}
private interface ScriptingCacheWithCounters : CompiledJvmScriptsCache {
val storedScripts: Int
val retrievedScripts: Int
}
private class SimpleMemoryScriptsCache : ScriptingCacheWithCounters {
internal val data = hashMapOf<Pair<SourceCode, ScriptCompilationConfiguration>, CompiledScript<*>>()
private var _storedScripts = 0
private var _retrievedScripts = 0
override val storedScripts: Int
get() = _storedScripts
override val retrievedScripts: Int
get() = _retrievedScripts
override fun get(script: SourceCode, scriptCompilationConfiguration: ScriptCompilationConfiguration): CompiledScript<*>? =
data[script to scriptCompilationConfiguration]?.also { _retrievedScripts++ }
override fun store(
compiledScript: CompiledScript<*>,
script: SourceCode,
scriptCompilationConfiguration: ScriptCompilationConfiguration
) {
data[script to scriptCompilationConfiguration] = compiledScript
_storedScripts++
}
}
private fun ScriptCompilationConfiguration.Builder.makeSimpleConfigurationWithTestImport() {
internal fun ScriptCompilationConfiguration.Builder.makeSimpleConfigurationWithTestImport() {
refineConfiguration {
beforeCompiling { ctx ->
val importedScript = File(ScriptingHostTest.TEST_DATA_DIR, "importTest/helloWithVal.kts")
@@ -563,58 +435,6 @@ private fun ScriptCompilationConfiguration.Builder.makeSimpleConfigurationWithTe
}
}
private fun File.readCompiledScript(scriptCompilationConfiguration: ScriptCompilationConfiguration): CompiledScript<*> {
return inputStream().use { fs ->
ObjectInputStream(fs).use {
it.readObject() as KJvmCompiledScript<*>
}
}
}
private fun ByteArray.toHexString(): String = joinToString("", transform = { "%02x".format(it) })
private class FileBasedScriptCache(val baseDir: File) : ScriptingCacheWithCounters {
internal fun uniqueHash(script: SourceCode, scriptCompilationConfiguration: ScriptCompilationConfiguration): String {
val digestWrapper = MessageDigest.getInstance("MD5")
digestWrapper.update(script.text.toByteArray())
scriptCompilationConfiguration.entries().sortedBy { it.key.name }.forEach {
digestWrapper.update(it.key.name.toByteArray())
digestWrapper.update(it.value.toString().toByteArray())
}
return digestWrapper.digest().toHexString()
}
override fun get(script: SourceCode, scriptCompilationConfiguration: ScriptCompilationConfiguration): CompiledScript<*>? {
val file = File(baseDir, uniqueHash(script, scriptCompilationConfiguration))
return if (!file.exists()) null else file.readCompiledScript(scriptCompilationConfiguration)?.also { _retrievedScripts++ }
}
override fun store(
compiledScript: CompiledScript<*>,
script: SourceCode,
scriptCompilationConfiguration: ScriptCompilationConfiguration
) {
val file = File(baseDir, uniqueHash(script, scriptCompilationConfiguration))
file.outputStream().use { fs ->
ObjectOutputStream(fs).use { os ->
os.writeObject(compiledScript)
}
}
_storedScripts++
}
private var _storedScripts = 0
private var _retrievedScripts = 0
override val storedScripts: Int
get() = _storedScripts
override val retrievedScripts: Int
get() = _retrievedScripts
}
internal fun captureOut(body: () -> Unit): String = captureOutAndErr(body).first
internal fun captureOutAndErr(body: () -> Unit): Pair<String, String> {