[Test] Add ability to run box test in separate jvm for common codegen tests

This commit is contained in:
Dmitriy Novozhilov
2021-07-14 12:57:07 +03:00
committed by teamcityserver
parent 04d9d243de
commit ca214bef30
5 changed files with 217 additions and 50 deletions
@@ -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,
@@ -61,6 +61,9 @@ class TestFile(
val name: String = relativePath.split("/").last()
}
val TestFile.nameWithoutExtension: String
get() = name.substringBeforeLast(".")
enum class DependencyRelation {
RegularDependency,
FriendDependency,
@@ -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<Class<*>, 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<URL> {
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
}
@@ -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 <R : ResultingArtifact.FrontendOutput<R>> TestConfigurationBuilder.commonConfigurationForCodegenTest(
targetFrontend: FrontendKind<*>,
@@ -45,6 +46,7 @@ fun <R : ResultingArtifact.FrontendOutput<R>> TestConfigurationBuilder.commonCon
::AdditionalDiagnosticsSourceFilesProvider,
::CoroutineHelpersSourceFilesProvider,
::CodegenHelpersSourceFilesProvider,
::MainFunctionForBlackBoxTestsSourceProvider,
)
useFrontendFacades(frontendFacade)
@@ -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<TestFile> {
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())
}
}