diff --git a/compiler/testData/codegen/customScript/stringReceiver.kts b/compiler/testData/codegen/customScript/stringReceiver.kts new file mode 100644 index 00000000000..bda94458d8b --- /dev/null +++ b/compiler/testData/codegen/customScript/stringReceiver.kts @@ -0,0 +1,8 @@ + +// KOTLIN_SCRIPT_DEFINITION: org.jetbrains.kotlin.codegen.TestScriptWithReceivers + +// receiver: abracadabra + +val res = drop(4) + +// expected: res = cadabra diff --git a/compiler/tests-common/build.gradle.kts b/compiler/tests-common/build.gradle.kts index f42092dd065..bb87198ee9b 100644 --- a/compiler/tests-common/build.gradle.kts +++ b/compiler/tests-common/build.gradle.kts @@ -28,6 +28,9 @@ dependencies { testCompile(projectTests(":compiler:tests-common-jvm6")) testCompileOnly(project(":kotlin-reflect-api")) testCompile(commonDep("junit:junit")) + testCompile(project(":kotlin-scripting-compiler")) + testCompile(project(":kotlin-scripting-misc")) + testCompile(project(":kotlin-script-util")) testCompile(androidDxJar()) { isTransitive = false } testCompile(intellijCoreDep()) { includeJars("intellij-core"); isTransitive = false } testCompile(intellijDep()) { diff --git a/compiler/tests-common/build.gradle.kts.173 b/compiler/tests-common/build.gradle.kts.173 index 6fb1115857d..2d930548955 100644 --- a/compiler/tests-common/build.gradle.kts.173 +++ b/compiler/tests-common/build.gradle.kts.173 @@ -28,6 +28,9 @@ dependencies { testCompile(projectTests(":compiler:tests-common-jvm6")) testCompileOnly(project(":kotlin-reflect-api")) testCompile(commonDep("junit:junit")) + testCompile(project(":kotlin-scripting-compiler")) + testCompile(project(":kotlin-scripting-misc")) + testCompile(project(":kotlin-script-util")) testCompile(androidDxJar()) { isTransitive = false } testCompile(intellijCoreDep()) { includeJars("intellij-core"); isTransitive = false } testCompile(intellijDep()) { diff --git a/compiler/tests-common/build.gradle.kts.182 b/compiler/tests-common/build.gradle.kts.182 index f496aae35c4..f32d5dcfe80 100644 --- a/compiler/tests-common/build.gradle.kts.182 +++ b/compiler/tests-common/build.gradle.kts.182 @@ -28,6 +28,9 @@ dependencies { testCompile(projectTests(":compiler:tests-common-jvm6")) testCompileOnly(project(":kotlin-reflect-api")) testCompile(commonDep("junit:junit")) + testCompile(project(":kotlin-scripting-compiler")) + testCompile(project(":kotlin-scripting-misc")) + testCompile(project(":kotlin-script-util")) testCompile(androidDxJar()) { isTransitive = false } testCompile(intellijCoreDep()) { includeJars("intellij-core"); isTransitive = false } testCompile(intellijDep()) { diff --git a/compiler/tests-common/build.gradle.kts.as32 b/compiler/tests-common/build.gradle.kts.as32 index 2b3d29f394d..e8e570c7e49 100644 --- a/compiler/tests-common/build.gradle.kts.as32 +++ b/compiler/tests-common/build.gradle.kts.as32 @@ -28,6 +28,9 @@ dependencies { testCompile(projectTests(":compiler:tests-common-jvm6")) testCompileOnly(project(":kotlin-reflect-api")) testCompile(commonDep("junit:junit")) + testCompile(project(":kotlin-scripting-compiler")) + testCompile(project(":kotlin-scripting-misc")) + testCompile(project(":kotlin-script-util")) testCompile(androidDxJar()) { isTransitive = false } testCompile(intellijCoreDep()) { includeJars("intellij-core"); isTransitive = false } testCompile(intellijDep()) { diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/AbstractCustomScriptCodegenTest.kt b/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/AbstractCustomScriptCodegenTest.kt new file mode 100644 index 00000000000..4e4fb95d27d --- /dev/null +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/AbstractCustomScriptCodegenTest.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.codegen + +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.compiler.NoScopeRecordCliBindingTrace +import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.AnalyzingUtils +import org.jetbrains.kotlin.script.util.scriptCompilationClasspathFromContextOrStlib +import org.jetbrains.kotlin.scripting.compiler.plugin.configureScriptDefinitions +import org.jetbrains.kotlin.test.ConfigurationKind +import org.jetbrains.kotlin.test.KotlinTestUtils +import org.jetbrains.kotlin.test.TestJdkKind +import org.junit.Assert +import java.io.File +import kotlin.reflect.full.starProjectedType +import kotlin.script.experimental.annotations.KotlinScript +import kotlin.script.experimental.annotations.KotlinScriptDefaultCompilationConfiguration +import kotlin.script.experimental.api.ScriptCompileConfigurationProperties +import kotlin.script.experimental.misc.invoke +import kotlin.script.experimental.util.TypedKey + +/* + Note by Ilya Chernikov: I gave up attempts to reuse the main compilation and testing logic from CodegenTestCase: it is too rigid + and ad-hoc. I ended up reimplementing (with some copy/paste) a subset relevant for scripting, but in (I assume) much more composable + and generic way. + I suggest that we will start building a parallel implementation of the CodegenTestCase taking composability much more seriously, + avoiding shared state and other rotten OOP misfeatures as much as possible. This implementation could be a starting point, unless + we'll find something better, of course. + */ + +open class AbstractCustomScriptCodegenTest : CodegenTestCase() { + + // TODO: add types to receivers, envVars and params + class ScriptTestFile(val file: File) { + + val content by lazy { file.readText() } + + val definitions: List by lazy { extractAllSimpleValues("KOTLIN_SCRIPT_DEFINITION") } + + val receivers: List by lazy { extractAllSimpleValues("receiver") } + + val environmentVars: Map by lazy { extractAllKeyValPairs("envVar") } + + val expected: Map by lazy { extractAllKeyValPairs("expected") } + + val scriptParams: List by lazy { extractAllSimpleValues("param") } + + private fun extractAllSimpleValues(directive: String) = + Regex("//\\s*$directive:\\s*([^\\s]+)").findAll(content).map { it.groupValues[1] }.toList() + + private fun extractAllKeyValPairs(directive: String) = + Regex("//\\s*$directive:\\s*([^\\s]+)\\s*=\\s*([^\\s]+)").findAll(content).map { it.groupValues[1] to it.groupValues[2] }.toMap() + } + + fun createEnvironment( + kind: ConfigurationKind, + jdkKind: TestJdkKind, + scriptDefinitions: List + ): KotlinCoreEnvironment { + + val classpath = + if (scriptDefinitions.isNotEmpty()) { + scriptCompilationClasspathFromContextOrStlib("tests-common", "kotlin-stdlib").also { + additionalDependencies = it + } + } else { + emptyList() + } + + val configuration = KotlinTestUtils.newConfiguration(kind, jdkKind, classpath, emptyList()) + + if (scriptDefinitions.isNotEmpty()) { + configureScriptDefinitions(scriptDefinitions, configuration, MessageCollector.NONE, emptyMap()) + } + + return KotlinCoreEnvironment.createForTests( + testRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES + ).also { + myEnvironment = it + } + } + + override fun doTest(filename: String) { + val testFile = AbstractCustomScriptCodegenTest.ScriptTestFile(File(filename)) + val environment = createEnvironment(ConfigurationKind.ALL, TestJdkKind.FULL_JDK, testFile.definitions) + + val psiFile = KotlinTestUtils.createFile(testFile.file.name, testFile.content, environment.project).also { + val ranges = AnalyzingUtils.getSyntaxErrorRanges(it) + assert(ranges.isEmpty()) { "Syntax errors found in $filename: $ranges" } + } + + val classesFactory = generateClasses(listOf(psiFile)) + if (classesFactory == null) { + fail("No class file was generated for: $name") + } + + try { + val scriptClass = classesFactory!!.loadClass(psiFile.script!!.fqName.asString()) + + val scriptInstance = runScript(scriptClass, testFile.receivers, testFile.environmentVars, testFile.scriptParams) + + checkExpectedFields(testFile.expected, scriptClass, scriptInstance) + + } catch (e: Throwable) { + println(classesFactory!!.createText()) + throw e + } + } + + private fun runScript(scriptClass: Class<*>, receivers: List, environmentVars: Map, scriptParams: List): Any? { + + val ctorParams = arrayListOf() + if (receivers.isNotEmpty()) { + ctorParams.add(receivers.toTypedArray()) + } + if (environmentVars.isNotEmpty()) { + ctorParams.add(environmentVars) + } + ctorParams.addAll(scriptParams) + + val constructor = scriptClass.constructors[0] + return constructor.newInstance(*ctorParams.toTypedArray()) + } + + protected fun checkExpectedFields(expectedFields: Map, scriptClass: Class<*>, scriptInstance: Any?) { + Assert.assertFalse("expecting at least one expectation", expectedFields.isEmpty()) + + for ((fieldName, expectedValue) in expectedFields) { + + if (expectedValue == "") { + try { + scriptClass.getDeclaredField(fieldName) + Assert.fail("must have no field $fieldName") + } catch (e: NoSuchFieldException) { + continue + } + } + + val field = scriptClass.getDeclaredField(fieldName) + field.isAccessible = true + val resultString = field.get(scriptInstance)?.toString() ?: "null" + Assert.assertEquals("comparing field $fieldName", expectedValue, resultString) + } + } + + protected fun ClassFileFactory.makeClassloader(): ClassLoader = + GeneratedClassLoader( + this, + when { + configurationKind.withReflection -> ForTestCompileRuntime.runtimeAndReflectJarClassLoader() + configurationKind.withCoroutines -> ForTestCompileRuntime.runtimeAndCoroutinesJarClassLoader() + else -> ForTestCompileRuntime.runtimeJarClassLoader() + }, + *classPathURLs + ) + + protected fun ClassFileFactory.loadClass(name: String): Class<*> = + makeClassloader().also { + if (!verifyAllFilesWithAsm(this, it)) { + Assert.fail("Verification failed: see exceptions above") + } + }.loadClass(name) + + protected fun generateClasses(psiFiles: List): ClassFileFactory? { + try { + val generationState = GenerationUtils.compileFiles( + psiFiles, myEnvironment, classBuilderFactory, + NoScopeRecordCliBindingTrace() + ) + val classFileFactory = generationState.factory + + if (verifyWithDex() && DxChecker.RUN_DX_CHECKER) { + DxChecker.check(classFileFactory) + } + return classFileFactory + } catch (e: Throwable) { + e.printStackTrace() + System.err.println("Generating instructions as text...") + try { + if (classFileFactory == null) { + System.err.println("Cannot generate text: exception was thrown during generation") + } else { + System.err.println(classFileFactory.createText()) + } + } catch (e1: Throwable) { + System.err.println("Exception thrown while trying to generate text, the actual exception follows:") + e1.printStackTrace() + System.err.println("-----------------------------------------------------------------------------") + } + + Assert.fail("See exceptions above") + return null + } + } +} + +object TestScriptWithReceiversConfiguration : ArrayList, Any?>>( + listOf( + ScriptCompileConfigurationProperties.scriptImplicitReceivers(String::class.starProjectedType) + ) +) + +@Suppress("unused") +@KotlinScript +@KotlinScriptDefaultCompilationConfiguration(TestScriptWithReceiversConfiguration::class) +abstract class TestScriptWithReceivers + diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/CodegenTestCase.java b/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/CodegenTestCase.java index 27c9531145c..0a553e1ebaf 100644 --- a/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/CodegenTestCase.java +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/CodegenTestCase.java @@ -518,7 +518,7 @@ public abstract class CodegenTestCase extends KtUsefulTestCase { return true; } - private static boolean verifyAllFilesWithAsm(ClassFileFactory factory, ClassLoader loader) { + protected static boolean verifyAllFilesWithAsm(ClassFileFactory factory, ClassLoader loader) { boolean noErrors = true; for (OutputFile file : ClassFileUtilsKt.getClassFiles(factory)) { noErrors &= verifyWithAsm(file, loader); diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/CustomScriptCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/CustomScriptCodegenTestGenerated.java new file mode 100644 index 00000000000..2ac780f31d6 --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/codegen/CustomScriptCodegenTestGenerated.java @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.codegen; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TargetBackend; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("compiler/testData/codegen/customScript") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class CustomScriptCodegenTestGenerated extends AbstractCustomScriptCodegenTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInCustomScript() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/customScript"), Pattern.compile("^(.+)\\.kts$"), TargetBackend.ANY, true); + } + + @TestMetadata("stringReceiver.kts") + public void testStringReceiver() throws Exception { + runTest("compiler/testData/codegen/customScript/stringReceiver.kts"); + } +} diff --git a/compiler/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerTests.kt b/compiler/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerTests.kt index 7d81aaf4ffd..1f4644275dd 100644 --- a/compiler/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerTests.kt +++ b/compiler/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerTests.kt @@ -181,6 +181,10 @@ fun main(args: Array) { model("codegen/script", extension = "kts") } + testClass { + model("codegen/customScript", extension = "kts") + } + testClass { model("codegen/bytecodeText") }