From ca214bef30a1b9c8f83c7c7d2a63f9fc4c88eaee Mon Sep 17 00:00:00 2001 From: Dmitriy Novozhilov Date: Wed, 14 Jul 2021 12:57:07 +0300 Subject: [PATCH] [Test] Add ability to run box test in separate jvm for common codegen tests --- .../org/jetbrains/kotlin/test/TestJdkKind.kt | 10 +- .../jetbrains/kotlin/test/model/Modules.kt | 3 + .../test/backend/handlers/JvmBoxRunner.kt | 190 +++++++++++++----- .../codegen/BaseCodegenConfiguration.kt | 2 + ...nFunctionForBlackBoxTestsSourceProvider.kt | 62 ++++++ 5 files changed, 217 insertions(+), 50 deletions(-) create mode 100644 compiler/tests-common-new/tests/org/jetbrains/kotlin/test/services/sourceProviders/MainFunctionForBlackBoxTestsSourceProvider.kt diff --git a/compiler/test-infrastructure-utils/tests/org/jetbrains/kotlin/test/TestJdkKind.kt b/compiler/test-infrastructure-utils/tests/org/jetbrains/kotlin/test/TestJdkKind.kt index 76c7a1ee7a4..b6cea72b109 100644 --- a/compiler/test-infrastructure-utils/tests/org/jetbrains/kotlin/test/TestJdkKind.kt +++ b/compiler/test-infrastructure-utils/tests/org/jetbrains/kotlin/test/TestJdkKind.kt @@ -15,7 +15,7 @@ */ package org.jetbrains.kotlin.test -enum class TestJdkKind { +enum class TestJdkKind(val requiresSeparateProcess: Boolean = false) { MOCK_JDK, // Differs from common mock JDK only by one additional 'nonExistingMethod' in Collection and constructor from Double in Throwable @@ -24,16 +24,16 @@ enum class TestJdkKind { MODIFIED_MOCK_JDK, // JDK found at $JDK_16 - FULL_JDK_6, + FULL_JDK_6(requiresSeparateProcess = true), // JDK found at $JDK_9 - FULL_JDK_9, + FULL_JDK_9(requiresSeparateProcess = true), // JDK found at $JDK_15 - FULL_JDK_15, + FULL_JDK_15(requiresSeparateProcess = true), // JDK found at $JDK_15 - FULL_JDK_17, + FULL_JDK_17(requiresSeparateProcess = true), // JDK found at java.home FULL_JDK, diff --git a/compiler/test-infrastructure/tests/org/jetbrains/kotlin/test/model/Modules.kt b/compiler/test-infrastructure/tests/org/jetbrains/kotlin/test/model/Modules.kt index 10ee95462d4..242a16fb691 100644 --- a/compiler/test-infrastructure/tests/org/jetbrains/kotlin/test/model/Modules.kt +++ b/compiler/test-infrastructure/tests/org/jetbrains/kotlin/test/model/Modules.kt @@ -61,6 +61,9 @@ class TestFile( val name: String = relativePath.split("/").last() } +val TestFile.nameWithoutExtension: String + get() = name.substringBeforeLast(".") + enum class DependencyRelation { RegularDependency, FriendDependency, diff --git a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/backend/handlers/JvmBoxRunner.kt b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/backend/handlers/JvmBoxRunner.kt index 8328472dd0f..d9cf2074817 100644 --- a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/backend/handlers/JvmBoxRunner.kt +++ b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/backend/handlers/JvmBoxRunner.kt @@ -11,20 +11,30 @@ import org.jetbrains.kotlin.codegen.* import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil.getFileClassInfoNoResolve import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.test.TestJdkKind import org.jetbrains.kotlin.test.clientserver.TestProxy import org.jetbrains.kotlin.test.directives.CodegenTestDirectives +import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.JDK_KIND +import org.jetbrains.kotlin.test.directives.model.singleOrZeroValue import org.jetbrains.kotlin.test.model.BinaryArtifacts import org.jetbrains.kotlin.test.model.DependencyKind import org.jetbrains.kotlin.test.model.TestModule +import org.jetbrains.kotlin.test.model.nameWithoutExtension import org.jetbrains.kotlin.test.services.* import org.jetbrains.kotlin.test.services.configuration.JvmEnvironmentConfigurator.Companion.TEST_CONFIGURATION_KIND_KEY import org.jetbrains.kotlin.test.services.jvm.compiledClassesManager +import org.jetbrains.kotlin.test.services.sourceProviders.MainFunctionForBlackBoxTestsSourceProvider +import org.jetbrains.kotlin.test.services.sourceProviders.MainFunctionForBlackBoxTestsSourceProvider.Companion.containsBoxMethod +import org.jetbrains.kotlin.test.util.KtTestUtil import org.jetbrains.kotlin.utils.addIfNotNull import org.jetbrains.kotlin.utils.addToStdlib.runIf import java.io.File import java.lang.reflect.Method +import java.net.URI import java.net.URL import java.net.URLClassLoader +import java.nio.file.Paths +import java.util.concurrent.TimeUnit class JvmBoxRunner(testServices: TestServices) : JvmBinaryArtifactHandler(testServices) { companion object { @@ -45,24 +55,18 @@ class JvmBoxRunner(testServices: TestServices) : JvmBinaryArtifactHandler(testSe val classLoader = createAndVerifyClassLoader(module, info.classFileFactory, reportProblems) try { for (ktFile in ktFiles) { - val className = ktFile.getFacadeFqName() ?: continue - val clazz = try { - classLoader.getGeneratedClass(className) - } catch (e: LinkageError) { - throw AssertionError("Failed to load class '$className':\n${info.classFileFactory.createText()}", e) + if (containsBoxMethod(ktFile.text)) { + boxMethodFound = true + callBoxMethodAndCheckResultWithCleanup( + ktFile, + module, + info.classFileFactory, + classLoader, + unexpectedBehaviour = false, + reportProblems = reportProblems + ) + return } - val method = clazz.getBoxMethodOrNull() ?: continue - boxMethodFound = true - callBoxMethodAndCheckResultWithCleanup( - module, - info.classFileFactory, - classLoader, - clazz, - method, - unexpectedBehaviour = false, - reportProblems = reportProblems - ) - return } } finally { classLoader.dispose() @@ -70,16 +74,15 @@ class JvmBoxRunner(testServices: TestServices) : JvmBinaryArtifactHandler(testSe } private fun callBoxMethodAndCheckResultWithCleanup( + ktFile: KtFile, module: TestModule, classFileFactory: ClassFileFactory, classLoader: URLClassLoader, - clazz: Class<*>, - method: Method, unexpectedBehaviour: Boolean, reportProblems: Boolean ) { try { - callBoxMethodAndCheckResult(module, classFileFactory, classLoader, clazz, method, unexpectedBehaviour) + callBoxMethodAndCheckResult(ktFile, module, classFileFactory, classLoader, unexpectedBehaviour) } catch (e: Throwable) { if (reportProblems) { try { @@ -97,28 +100,44 @@ class JvmBoxRunner(testServices: TestServices) : JvmBinaryArtifactHandler(testSe } } + private fun findClassAndMethodToExecute( + ktFile: KtFile, + classLoader: URLClassLoader, + classFileFactory: ClassFileFactory + ): Pair, Method> { + val className = ktFile.getFacadeFqName() ?: error("Class<*> for ${ktFile.name} not found") + val clazz = try { + classLoader.getGeneratedClass(className) + } catch (e: LinkageError) { + throw AssertionError("Failed to load class '$className':\n${classFileFactory.createText()}", e) + } + val method = clazz.getBoxMethodOrNull() ?: error("box method not found") + return clazz to method + } + private fun callBoxMethodAndCheckResult( + ktFile: KtFile, module: TestModule, classFileFactory: ClassFileFactory, classLoader: URLClassLoader, - clazz: Class<*>, - method: Method, unexpectedBehaviour: Boolean ) { val result = if (BOX_IN_SEPARATE_PROCESS_PORT != null) { - invokeBoxInSeparateProcess(module, classFileFactory, classLoader, clazz) + invokeBoxInSeparateProcess( + module, + classFileFactory, + classLoader, + findClassAndMethodToExecute(ktFile, classLoader, classFileFactory).first + ) } else { - val savedClassLoader = Thread.currentThread().contextClassLoader - if (savedClassLoader !== classLoader) { - // otherwise the test infrastructure used in the test may conflict with the one from the context classloader - Thread.currentThread().contextClassLoader = classLoader - } - try { - method.invoke(null) as String - } finally { - if (savedClassLoader !== classLoader) { - Thread.currentThread().contextClassLoader = savedClassLoader - } + val jdkKind = module.directives.singleOrZeroValue(JDK_KIND) + if (jdkKind?.requiresSeparateProcess == true) { + runSeparateJvmInstance(module, jdkKind, classLoader, classFileFactory) + } else { + runBoxInCurrentProcess( + classLoader, + findClassAndMethodToExecute(ktFile, classLoader, classFileFactory).second + ) } } if (unexpectedBehaviour) { @@ -128,24 +147,103 @@ class JvmBoxRunner(testServices: TestServices) : JvmBinaryArtifactHandler(testSe } } + private fun runBoxInCurrentProcess( + classLoader: URLClassLoader, + method: Method, + ): String { + val savedClassLoader = Thread.currentThread().contextClassLoader + if (savedClassLoader !== classLoader) { + // otherwise the test infrastructure used in the test may conflict with the one from the context classloader + Thread.currentThread().contextClassLoader = classLoader + } + return try { + method.invoke(null) as String + } finally { + if (savedClassLoader !== classLoader) { + Thread.currentThread().contextClassLoader = savedClassLoader + } + } + } + + /* + * TODO: + * Running separate jvm for each test may be very expensive in case + * there will be a lot of tests which use this feature, so we should + * consider to run single jvm as proxy (see [invokeBoxInSeparateProcess]) + */ + private fun runSeparateJvmInstance( + module: TestModule, + jdkKind: TestJdkKind, + classLoader: URLClassLoader, + classFileFactory: ClassFileFactory + ): String { + val jdkHome = when (jdkKind) { + TestJdkKind.FULL_JDK_9 -> KtTestUtil.getJdk9Home() + TestJdkKind.FULL_JDK_15 -> KtTestUtil.getJdk15Home() + TestJdkKind.FULL_JDK_17 -> KtTestUtil.getJdk17Home() + else -> error("Unsupported JDK kind: $jdkKind") + } + + val javaExe = File(jdkHome, "bin/java.exe").takeIf(File::exists) + ?: File(jdkHome, "bin/java").takeIf(File::exists) + ?: error("Can't find 'java' executable in $jdkHome") + + val classPath = extractClassPath(module, classLoader, classFileFactory) + + val mainFile = module.files.firstOrNull { + it.name == MainFunctionForBlackBoxTestsSourceProvider.BOX_MAIN_FILE_NAME && it.isAdditional + } ?: error("No file with main function was generated. Please check TODO source provider") + + val mainFqName = listOfNotNull( + MainFunctionForBlackBoxTestsSourceProvider.detectPackage(mainFile), + "${mainFile.nameWithoutExtension}Kt" + ).joinToString(".") + + val command = arrayOf( + javaExe.absolutePath, + "-ea", + "-classpath", + classPath.joinToString(File.pathSeparator, transform = { File(it.toURI()).absolutePath }), + mainFqName, + ) + val process = ProcessBuilder(*command).inheritIO().start() + process.waitFor(1, TimeUnit.MINUTES) + process.outputStream.flush() + return when (process.exitValue()) { + 0 -> "OK" + else -> "External process completed with error. Check the build log" + } + } + private fun invokeBoxInSeparateProcess( module: TestModule, classFileFactory: ClassFileFactory, classLoader: URLClassLoader, clazz: Class<*> ): String { - val classPath = classLoader.extractUrls().toMutableList() - if (classLoader is GeneratedClassLoader) { - val javaPath = testServices.compiledClassesManager.getCompiledJavaDirForModule(module)?.url - if (javaPath != null) { - classPath.add(0, javaPath) - } - classPath.add(0, testServices.compiledClassesManager.getCompiledKotlinDirForModule(module, classFileFactory).url) - } + val classPath = extractClassPath(module, classLoader, classFileFactory) val proxy = TestProxy(Integer.valueOf(BOX_IN_SEPARATE_PROCESS_PORT), clazz.canonicalName, classPath) return proxy.runTest() } + @OptIn(ExperimentalStdlibApi::class) + private fun extractClassPath( + module: TestModule, + classLoader: URLClassLoader, + classFileFactory: ClassFileFactory + ): List { + return buildList { + addAll(classLoader.extractUrls()) + if (classLoader is GeneratedClassLoader) { + val javaPath = testServices.compiledClassesManager.getCompiledJavaDirForModule(module)?.url + if (javaPath != null) { + add(0, javaPath) + } + add(0, testServices.compiledClassesManager.getCompiledKotlinDirForModule(module, classFileFactory).url) + } + } + } + private val File.url: URL get() = toURI().toURL() @@ -155,9 +253,11 @@ class JvmBoxRunner(testServices: TestServices) : JvmBinaryArtifactHandler(testSe reportProblems: Boolean ): GeneratedClassLoader { val classLoader = createClassLoader(module, classFileFactory) - val verificationSucceeded = CodegenTestUtil.verifyAllFilesWithAsm(classFileFactory, classLoader, reportProblems) - if (!verificationSucceeded) { - assertions.fail { "Verification failed: see exceptions above" } + if (module.directives.singleOrZeroValue(JDK_KIND)?.requiresSeparateProcess != true) { + val verificationSucceeded = CodegenTestUtil.verifyAllFilesWithAsm(classFileFactory, classLoader, reportProblems) + if (!verificationSucceeded) { + assertions.fail { "Verification failed: see exceptions above" } + } } return classLoader } diff --git a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/runners/codegen/BaseCodegenConfiguration.kt b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/runners/codegen/BaseCodegenConfiguration.kt index dae24b57ba7..cb478e3d13a 100644 --- a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/runners/codegen/BaseCodegenConfiguration.kt +++ b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/runners/codegen/BaseCodegenConfiguration.kt @@ -18,6 +18,7 @@ import org.jetbrains.kotlin.test.services.configuration.ScriptingEnvironmentConf import org.jetbrains.kotlin.test.services.sourceProviders.AdditionalDiagnosticsSourceFilesProvider import org.jetbrains.kotlin.test.services.sourceProviders.CodegenHelpersSourceFilesProvider import org.jetbrains.kotlin.test.services.sourceProviders.CoroutineHelpersSourceFilesProvider +import org.jetbrains.kotlin.test.services.sourceProviders.MainFunctionForBlackBoxTestsSourceProvider fun > TestConfigurationBuilder.commonConfigurationForCodegenTest( targetFrontend: FrontendKind<*>, @@ -45,6 +46,7 @@ fun > TestConfigurationBuilder.commonCon ::AdditionalDiagnosticsSourceFilesProvider, ::CoroutineHelpersSourceFilesProvider, ::CodegenHelpersSourceFilesProvider, + ::MainFunctionForBlackBoxTestsSourceProvider, ) useFrontendFacades(frontendFacade) diff --git a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/services/sourceProviders/MainFunctionForBlackBoxTestsSourceProvider.kt b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/services/sourceProviders/MainFunctionForBlackBoxTestsSourceProvider.kt new file mode 100644 index 00000000000..54d935212de --- /dev/null +++ b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/services/sourceProviders/MainFunctionForBlackBoxTestsSourceProvider.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2021 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 org.jetbrains.kotlin.test.services.sourceProviders + +import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.JDK_KIND +import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives +import org.jetbrains.kotlin.test.directives.model.singleOrZeroValue +import org.jetbrains.kotlin.test.model.TestFile +import org.jetbrains.kotlin.test.model.TestModule +import org.jetbrains.kotlin.test.services.AdditionalSourceProvider +import org.jetbrains.kotlin.test.services.TestServices +import org.jetbrains.kotlin.test.services.temporaryDirectoryManager + +class MainFunctionForBlackBoxTestsSourceProvider(testServices: TestServices) : AdditionalSourceProvider(testServices) { + companion object { + private val PACKAGE_REGEXP = """package ([\w.]+)""".toRegex() + private val START_BOX_METHOD_REGEX = """^fun box\(\)""".toRegex() + private val MIDDLE_BOX_METHOD_REGEX = """\nfun box\(\)""".toRegex() + + const val BOX_MAIN_FILE_NAME = "Generated_Box_Main.kt" + + fun detectPackage(file: TestFile): String? { + return PACKAGE_REGEXP.find(file.originalContent)?.groups?.get(1)?.value + } + + fun containsBoxMethod(file: TestFile): Boolean { + return containsBoxMethod(file.originalContent) + } + + fun containsBoxMethod(fileContent: String): Boolean { + return START_BOX_METHOD_REGEX.containsMatchIn(fileContent) || MIDDLE_BOX_METHOD_REGEX.containsMatchIn(fileContent) + } + } + + override fun produceAdditionalFiles(globalDirectives: RegisteredDirectives, module: TestModule): List { + val jdkKind = module.directives.singleOrZeroValue(JDK_KIND) ?: return emptyList() + if (!jdkKind.requiresSeparateProcess) return emptyList() + + val fileWithBox = module.files.firstOrNull { containsBoxMethod(it) } ?: return emptyList() + + val code = buildString { + detectPackage(fileWithBox)?.let { + appendLine("package $it") + } + appendLine( + """ + fun main() { + val res = box() + if (res != "OK") throw AssertionError(res) + } + """.trimIndent() + ) + } + val file = testServices.temporaryDirectoryManager.getOrCreateTempDirectory("src").resolve(BOX_MAIN_FILE_NAME) + file.writeText(code) + + return listOf(file.toTestFile()) + } +}