From ba4fab56a05249b940285aad4d17dad2f363e9bb Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Mon, 5 Sep 2016 17:06:44 +0200 Subject: [PATCH] Implement basic JSR 223 scripting support --- .../services/javax.script.ScriptEngineFactory | 1 + .../kotlin/cli/jvm/repl/GenericRepl.kt | 246 ++++++++++++++++++ .../cli/jvm/repl/KotlinJsr232ScriptEngine.kt | 98 +++++++ ...KotlinJsr232StandardScriptEngineFactory.kt | 71 +++++ .../kotlin/cli/jvm/repl/ReplInterpreter.kt | 2 - .../config/CommonConfigurationKeys.java | 3 - .../jvm/repl/KotlinJsr223ScriptEngineTest.kt | 79 ++++++ 7 files changed, 495 insertions(+), 5 deletions(-) create mode 100644 compiler/cli/src/META-INF/services/javax.script.ScriptEngineFactory create mode 100644 compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/GenericRepl.kt create mode 100644 compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr232ScriptEngine.kt create mode 100644 compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr232StandardScriptEngineFactory.kt create mode 100644 compiler/tests/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr223ScriptEngineTest.kt diff --git a/compiler/cli/src/META-INF/services/javax.script.ScriptEngineFactory b/compiler/cli/src/META-INF/services/javax.script.ScriptEngineFactory new file mode 100644 index 00000000000..3b8ec69135a --- /dev/null +++ b/compiler/cli/src/META-INF/services/javax.script.ScriptEngineFactory @@ -0,0 +1 @@ +org.jetbrains.kotlin.cli.jvm.repl.KotlinJsr232StandardScriptEngineFactory diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/GenericRepl.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/GenericRepl.kt new file mode 100644 index 00000000000..20e79644212 --- /dev/null +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/GenericRepl.kt @@ -0,0 +1,246 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.cli.jvm.repl + +import com.google.common.base.Throwables +import com.intellij.openapi.Disposable +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.vfs.CharsetToolkit +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.impl.PsiFileFactoryImpl +import com.intellij.testFramework.LightVirtualFile +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots +import org.jetbrains.kotlin.cli.jvm.repl.messages.DiagnosticMessageHolder +import org.jetbrains.kotlin.cli.jvm.repl.messages.ReplTerminalDiagnosticMessageHolder +import org.jetbrains.kotlin.codegen.ClassBuilderFactories +import org.jetbrains.kotlin.codegen.KotlinCodegenFacade +import org.jetbrains.kotlin.codegen.state.GenerationState +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.JVMConfigurationKeys +import org.jetbrains.kotlin.descriptors.ScriptDescriptor +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.parsing.KotlinParserDefinition +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.jvm.JvmClassName +import org.jetbrains.kotlin.script.KotlinScriptDefinition +import org.jetbrains.kotlin.script.KotlinScriptExternalDependencies +import org.jetbrains.kotlin.script.ScriptParameter +import java.net.URLClassLoader +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +private val logger = Logger.getInstance(GenericRepl::class.java) + +open class GenericRepl( + disposable: Disposable, + val scriptDefinition: KotlinScriptDefinition, + val compilerConfiguration: CompilerConfiguration, + messageCollector: MessageCollector +) { + private val environment = run { + compilerConfiguration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, DelegatingScriptDefWithNoParams(scriptDefinition)) + compilerConfiguration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector) + KotlinCoreEnvironment.createForProduction(disposable, compilerConfiguration, EnvironmentConfigFiles.JVM_CONFIG_FILES) + } + + private val psiFileFactory: PsiFileFactoryImpl = PsiFileFactory.getInstance(environment.project) as PsiFileFactoryImpl + private val analyzerEngine = CliReplAnalyzerEngine(environment) + + private class ChunkState( + val code: String, + val psiFile: KtFile, + val errorHolder: DiagnosticMessageHolder) + + sealed class EvalResult { + class ValueResult(val value: Any?): EvalResult() + + object UnitResult: EvalResult() + object Ready: EvalResult() + object Incomplete : EvalResult() + + sealed class Error(val errorText: String): EvalResult() { + class Runtime(errorText: String): Error(errorText) + class CompileTime(errorText: String): Error(errorText) + } + } + + private class DelegatingScriptDefWithNoParams(parent: KotlinScriptDefinition) : KotlinScriptDefinition by parent { + override fun getScriptParameters(scriptDescriptor: ScriptDescriptor): List = emptyList() + } + + private var chunkState: ChunkState? = null + + private var lastDependencies: KotlinScriptExternalDependencies? = null + + val classpath = compilerConfiguration.jvmClasspathRoots.toMutableList() + + private var classLoader: ReplClassLoader = + ReplClassLoader(URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), Thread.currentThread().contextClassLoader)) + private val classLoaderLock = ReentrantReadWriteLock() + + private val earlierLines = arrayListOf() + + fun createDiagnosticHolder() = ReplTerminalDiagnosticMessageHolder() + + fun checkComplete(executionNumber: Long, code: String): EvalResult { + synchronized(this) { + val virtualFile = + LightVirtualFile("line$executionNumber${KotlinParserDefinition.STD_SCRIPT_EXT}", KotlinLanguage.INSTANCE, code).apply { + charset = CharsetToolkit.UTF8_CHARSET + } + val psiFile: KtFile = psiFileFactory.trySetupPsiForFile(virtualFile, KotlinLanguage.INSTANCE, true, false) as KtFile? + ?: error("Script file not analyzed at line $executionNumber: $code") + + val errorHolder = createDiagnosticHolder() + + val syntaxErrorReport = AnalyzerWithCompilerReport.Companion.reportSyntaxErrors(psiFile, errorHolder) + + if (!syntaxErrorReport.isHasErrors) { + chunkState = ChunkState(code, psiFile, errorHolder) + } + + return when { + syntaxErrorReport.isHasErrors && syntaxErrorReport.isAllErrorsAtEof -> EvalResult.Incomplete + syntaxErrorReport.isHasErrors -> EvalResult.Error.CompileTime(errorHolder.renderedDiagnostics) + else -> EvalResult.Ready + } + } + } + + fun eval(executionNumber: Long, code: String): EvalResult { + synchronized(this) { + val (psiFile, errorHolder) = run { + if (chunkState == null || chunkState!!.code != code) { + val res = checkComplete(executionNumber, code) + if (res != EvalResult.Ready) return@eval res + } + Pair(chunkState!!.psiFile, chunkState!!.errorHolder) + } + + val newDependencies = scriptDefinition.getDependenciesFor(psiFile, environment.project, lastDependencies) + newDependencies?.let { + logger.debug("found dependencies: ${it.classpath}") + val newCp = environment.updateClasspath(it.classpath.map(::JvmClasspathRoot)) + if (newCp != null && newCp.isNotEmpty()) { + logger.debug("new dependencies: $newCp") + classLoaderLock.write { + classpath.addAll(newCp) + classLoader = ReplClassLoader(URLClassLoader(newCp.map { it.toURI().toURL() }.toTypedArray(), classLoader)) + } + } + } + if (lastDependencies != newDependencies) { + lastDependencies = newDependencies + } + + val analysisResult = analyzerEngine.analyzeReplLine(psiFile, executionNumber.toInt()) + AnalyzerWithCompilerReport.Companion.reportDiagnostics(analysisResult.diagnostics, errorHolder, false) + val scriptDescriptor = when (analysisResult) { + is CliReplAnalyzerEngine.ReplLineAnalysisResult.WithErrors -> return EvalResult.Error.CompileTime(errorHolder.renderedDiagnostics) + is CliReplAnalyzerEngine.ReplLineAnalysisResult.Successful -> analysisResult.scriptDescriptor + else -> error("Unexpected result ${analysisResult.javaClass}") + } + + val state = GenerationState( + psiFile.project, + ClassBuilderFactories.binaries(false), + analyzerEngine.module, + analyzerEngine.trace.bindingContext, + listOf(psiFile), + compilerConfiguration + ) + state.replSpecific.scriptResultFieldName = SCRIPT_RESULT_FIELD_NAME + state.replSpecific.earlierScriptsForReplInterpreter = earlierLines.map(EarlierLine::getScriptDescriptor) + state.beforeCompile() + KotlinCodegenFacade.generatePackage( + state, + psiFile.script!!.getContainingKtFile().packageFqName, + setOf(psiFile.script!!.getContainingKtFile()), + org.jetbrains.kotlin.codegen.CompilationErrorHandler.THROW_EXCEPTION) + + for (outputFile in state.factory.asList()) { + if (outputFile.relativePath.endsWith(".class")) { + classLoaderLock.read { + classLoader.addClass(JvmClassName.byInternalName(outputFile.relativePath.replaceFirst("\\.class$".toRegex(), "")), + outputFile.asByteArray()) + } + } + } + + try { + val scriptClass = classLoaderLock.read { classLoader.loadClass("Line$executionNumber") } + + val constructorParams = earlierLines.map(EarlierLine::getScriptClass).toTypedArray() + val constructorArgs = earlierLines.map(EarlierLine::getScriptInstance).toTypedArray() + + val scriptInstanceConstructor = scriptClass.getConstructor(*constructorParams) + val scriptInstance = + try { + evalWithIO { scriptInstanceConstructor.newInstance(*constructorArgs) } + } + catch (e: Throwable) { + // ignore everything in the stack trace until this constructor call + return EvalResult.Error.Runtime(renderStackTrace(e.cause!!, startFromMethodName = "${scriptClass.name}.")) + } + + val rvField = scriptClass.getDeclaredField(SCRIPT_RESULT_FIELD_NAME).apply { isAccessible = true } + val rv: Any? = rvField.get(scriptInstance) + + earlierLines.add(EarlierLine(code, scriptDescriptor, scriptClass, scriptInstance)) + + return if (state.replSpecific.hasResult) EvalResult.ValueResult(rv) else EvalResult.UnitResult + } + catch (e: Throwable) { + throw e + } + } + } + + // override to capture output + open fun evalWithIO(body: () -> T): T = body() + + companion object { + private val SCRIPT_RESULT_FIELD_NAME = "\$\$result" + + private fun renderStackTrace(cause: Throwable, startFromMethodName: String): String { + val newTrace = arrayListOf() + var skip = true + for ((i, element) in cause.stackTrace.withIndex().reversed()) { + if ("${element.className}.${element.methodName}" == startFromMethodName) { + skip = false + } + if (!skip) { + newTrace.add(element) + } + } + + val resultingTrace = newTrace.reversed().dropLast(1) + + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "UsePropertyAccessSyntax") + (cause as java.lang.Throwable).setStackTrace(resultingTrace.toTypedArray()) + + return Throwables.getStackTraceAsString(cause) + } + } +} diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr232ScriptEngine.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr232ScriptEngine.kt new file mode 100644 index 00000000000..e7992d61102 --- /dev/null +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr232ScriptEngine.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.cli.jvm.repl + +import com.intellij.openapi.Disposable +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.script.KotlinScriptDefinition +import java.io.Reader +import javax.script.* + +class KotlinJsr232ScriptEngine( + disposable: Disposable, + private val factory: KotlinJsr232StandardScriptEngineFactory, + private val scriptDefinition: KotlinScriptDefinition, + private val compilerConfiguration: CompilerConfiguration +) : AbstractScriptEngine(), ScriptEngine { + + data class MessageCollectorReport(val severity: CompilerMessageSeverity, val message: String, val location: CompilerMessageLocation) + + private val messageCollector = object : MessageCollector { + + private val messageRenderer = MessageRenderer.WITHOUT_PATHS + private var hasErrors = false + private val reports = arrayListOf() + + override fun clear() { + reports.clear() + } + + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation) { + hasErrors = hasErrors or severity.isError + reports.add(MessageCollectorReport(severity, message, location)) + } + + override fun hasErrors(): Boolean = hasErrors + + fun resetAndThrowOnErrors() { + try { + if (hasErrors) { + val msg = reports.joinToString("\n") { messageRenderer.render(it.severity, it.message, it.location) } + val firstErr = reports.firstOrNull { it.severity.isError } + if (firstErr != null) + throw ScriptException(msg, firstErr.location.path, firstErr.location.line, firstErr.location.column) + else + throw ScriptException(msg) + } + } + finally { + clear() + hasErrors = false + } + } + } + + private val repl = object : GenericRepl(disposable, scriptDefinition, compilerConfiguration, messageCollector) {} + + private var lineCount = 0L + + override fun eval(script: String, context: ScriptContext?): Any? { + lineCount += 1 + // TODO bind to context + val evalResult = repl.eval(lineCount, script) + messageCollector.resetAndThrowOnErrors() + val ret = when (evalResult) { + is GenericRepl.EvalResult.ValueResult -> evalResult.value + is GenericRepl.EvalResult.UnitResult -> null + is GenericRepl.EvalResult.Error -> throw ScriptException(evalResult.errorText) + is GenericRepl.EvalResult.Incomplete -> throw ScriptException("error: incomplete code") + else -> throw ScriptException("Unexpected result from eval call: $evalResult") + } + // TODO update context + return ret + } + + override fun eval(script: Reader, context: ScriptContext?): Any? = eval(script.readText(), context) + + override fun createBindings(): Bindings = SimpleBindings() + + override fun getFactory(): ScriptEngineFactory = factory +} diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr232StandardScriptEngineFactory.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr232StandardScriptEngineFactory.kt new file mode 100644 index 00000000000..a66f8ef02bd --- /dev/null +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr232StandardScriptEngineFactory.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.cli.jvm.repl + +import com.intellij.openapi.util.Disposer +import org.jetbrains.kotlin.cli.common.KotlinVersion +import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.JVMConfigurationKeys +import org.jetbrains.kotlin.script.StandardScriptDefinition +import org.jetbrains.kotlin.utils.PathUtil +import javax.script.ScriptEngine +import javax.script.ScriptEngineFactory + +@Suppress("unused") // used in javax.script.ScriptEngineFactory META-INF file +class KotlinJsr232StandardScriptEngineFactory: ScriptEngineFactory { + + override fun getLanguageName(): String = "kotlin" + override fun getLanguageVersion(): String = KotlinVersion.VERSION + override fun getEngineName(): String = "kotlin" + override fun getEngineVersion(): String = KotlinVersion.VERSION + override fun getExtensions(): List = listOf("kts") + override fun getMimeTypes(): List = listOf("text/x-kotlin") + override fun getNames(): List = listOf("kotlin") + + override fun getScriptEngine(): ScriptEngine = + KotlinJsr232ScriptEngine( + Disposer.newDisposable(), + this, + StandardScriptDefinition, + CompilerConfiguration().apply { + addJvmClasspathRoots(PathUtil.getJdkClassesRoots()) + addJvmClasspathRoots(PathUtil.getKotlinPathsForCompiler().let { listOf(it.runtimePath, it.reflectPath) }) + // TODO: addJvmClasspathRoots(config.classpath) + put(CommonConfigurationKeys.MODULE_NAME, "kotlin-script") + put(JVMConfigurationKeys.INCLUDE_RUNTIME, true) + }) + + override fun getOutputStatement(toDisplay: String?): String = "print(\"$toDisplay\")" + override fun getMethodCallSyntax(obj: String, m: String, vararg args: String): String = "$obj.$m(${args.joinToString()})" + + override fun getProgram(vararg statements: String): String { + val sep = System.getProperty("line.separator") + return statements.joinToString(sep) + sep + } + + override fun getParameter(key: String?): Any? = + when (key) { + ScriptEngine.NAME -> engineName + ScriptEngine.LANGUAGE -> languageName + ScriptEngine.LANGUAGE_VERSION -> languageVersion + ScriptEngine.ENGINE -> engineName + ScriptEngine.ENGINE_VERSION -> engineVersion + else -> null + } +} \ No newline at end of file diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.kt index 168e8e80b43..9cc9361913c 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.kt @@ -32,7 +32,6 @@ import org.jetbrains.kotlin.codegen.ClassBuilderFactories import org.jetbrains.kotlin.codegen.CompilationErrorHandler import org.jetbrains.kotlin.codegen.KotlinCodegenFacade import org.jetbrains.kotlin.codegen.state.GenerationState -import org.jetbrains.kotlin.config.CommonConfigurationKeys import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.JVMConfigurationKeys import org.jetbrains.kotlin.descriptors.ScriptDescriptor @@ -61,7 +60,6 @@ class ReplInterpreter( private val environment = run { configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, REPL_LINE_AS_SCRIPT_DEFINITION) - configuration.put(CommonConfigurationKeys.REPL_MODE, true) KotlinCoreEnvironment.createForProduction(disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/config/CommonConfigurationKeys.java b/compiler/frontend/src/org/jetbrains/kotlin/config/CommonConfigurationKeys.java index 5e25075e714..7501d3378dc 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/config/CommonConfigurationKeys.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/config/CommonConfigurationKeys.java @@ -28,7 +28,4 @@ public class CommonConfigurationKeys { public static final CompilerConfigurationKey MODULE_NAME = CompilerConfigurationKey.create("module name"); - - public static final CompilerConfigurationKey REPL_MODE = - CompilerConfigurationKey.create("REPL mode"); } diff --git a/compiler/tests/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr223ScriptEngineTest.kt b/compiler/tests/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr223ScriptEngineTest.kt new file mode 100644 index 00000000000..a1e21569e00 --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/cli/jvm/repl/KotlinJsr223ScriptEngineTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.cli.jvm.repl + +import junit.framework.TestCase +import org.jetbrains.kotlin.cli.common.KotlinVersion +import org.junit.Assert +import org.junit.Test +import javax.script.ScriptEngine +import javax.script.ScriptEngineFactory +import javax.script.ScriptEngineManager +import javax.script.SimpleBindings + +class KotlinJsr223ScriptEngineTest : TestCase() { + + private var factory: ScriptEngineFactory? = null + private var engine: ScriptEngine? = null + + override fun setUp() { + super.setUp() + factory = ScriptEngineManager().getEngineByExtension("kts").factory + engine = factory?.scriptEngine + } + + override fun tearDown() { + factory = null + super.tearDown() + } + + @Test + fun testEngineFactory() { + Assert.assertNotNull(factory) + factory!!.apply { + Assert.assertEquals("kotlin", languageName) + Assert.assertEquals(KotlinVersion.VERSION, languageVersion) + Assert.assertEquals("kotlin", engineName) + Assert.assertEquals(KotlinVersion.VERSION, engineVersion) + Assert.assertEquals(listOf("kts"), extensions) + Assert.assertEquals(listOf("text/x-kotlin"), mimeTypes) + Assert.assertEquals(listOf("kotlin"), names) + Assert.assertEquals("obj.method(arg1, arg2, arg3)", getMethodCallSyntax("obj", "method", "arg1", "arg2", "arg3")) + Assert.assertEquals("print(\"Hello, world!\")", getOutputStatement("Hello, world!")) + Assert.assertEquals(KotlinVersion.VERSION, getParameter(ScriptEngine.LANGUAGE_VERSION)) + val sep = System.getProperty("line.separator") + val prog = arrayOf("val x: Int = 3", "var y = x + 2") + Assert.assertEquals(prog.joinToString(sep) + sep, getProgram(*prog)) + } + } + + @Test + fun testEngine() { + Assert.assertNotNull(engine as? KotlinJsr232ScriptEngine) + Assert.assertSame(factory, engine!!.factory) + val bindings = engine!!.createBindings() + Assert.assertTrue(bindings is SimpleBindings) + } + + @Test + fun testSimpleEval() { + val res1 = engine!!.eval("val x = 3") + Assert.assertNull(res1) + val res2 = engine!!.eval("x + 2") + Assert.assertEquals(5, res2) + } +} \ No newline at end of file