From c7eb9e9520669473b6b648a47ba1a8b5b0d1b108 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Wed, 24 Jul 2019 12:02:30 +0200 Subject: [PATCH] Move caching tests into a dedicated class plus some tests refactorings --- .../experimental/jvmhost/test/CachingTest.kt | 205 ++++++++++++++++ .../jvmhost/test/ScriptingHostTest.kt | 222 ++---------------- 2 files changed, 226 insertions(+), 201 deletions(-) create mode 100644 libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/CachingTest.kt diff --git a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/CachingTest.kt b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/CachingTest.kt new file mode 100644 index 00000000000..f4d4b2bddf8 --- /dev/null +++ b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/CachingTest.kt @@ -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, + configurationBuilder: ScriptCompilationConfiguration.Builder.() -> Unit = {} + ) { + val compiler = JvmScriptCompiler(defaultJvmScriptingHostConfiguration, cache = cache) + val evaluator = BasicJvmScriptEvaluator() + val host = BasicJvmScriptingHost(compiler = compiler, evaluator = evaluator) + + val scriptCompilationConfiguration = + createJvmCompilationConfigurationFromTemplate(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, 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 withTempDir(keyName: String = "tmp", body: (File) -> R) { + val tempDir = Files.createTempDirectory(keyName).toFile() + try { + body(tempDir) + } finally { + tempDir.deleteRecursively() + } +} + diff --git a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt index b9191381363..9fccc30dd57 100644 --- a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt +++ b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt @@ -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, - configurationBuilder: ScriptCompilationConfiguration.Builder.() -> Unit = {} - ) { - val compiler = JvmScriptCompiler(defaultJvmScriptingHostConfiguration, cache = cache) - val evaluator = BasicJvmScriptEvaluator() - val host = BasicJvmScriptingHost(compiler = compiler, evaluator = evaluator) - - val scriptCompilationConfiguration = - createJvmCompilationConfigurationFromTemplate(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 ResultWithDiagnostics.throwOnFailure(): ResultWithDiagnostics = 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, 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 {