mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[gui-test] build test classpath only for tests
After extending {IDE}-guitests modules classpath with {IDE}-main module we became experiencing a problem with an abnormally wide classpath. For example, GUI test for the WebStorm starts IDE with extra plugins from PhpStorm and JavaEE. That was happened because of very deep dependency tree for the test scope. Since this commit, we are building a classpath only for a production scope of testing module and adding a set of test classes (directory containing test sources or a jar). All test util methods should be placed in a production scope. The other problem is to resolve all macros (MAVEN_REPOSITORY and KOTLIN_BUNDLED) for generated classpath for a testing module by JPS. it is solved by finding a similar URLs in a built classpath for a testing module and replace the macros in JPSs classpath.
This commit is contained in:
+53
-14
@@ -36,6 +36,7 @@ import org.junit.runner.notification.Failure
|
||||
import org.junit.runner.notification.RunListener
|
||||
import org.junit.runner.notification.RunNotifier
|
||||
import org.junit.runners.BlockJUnit4ClassRunner
|
||||
import org.junit.runners.Suite
|
||||
import org.junit.runners.model.FrameworkMethod
|
||||
import org.junit.runners.model.InitializationError
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -43,12 +44,17 @@ import kotlin.reflect.KClass
|
||||
|
||||
|
||||
class GuiTestLocalRunner @Throws(InitializationError::class)
|
||||
constructor(testClass: Class<*>, val ide: Ide?) : BlockJUnit4ClassRunner(testClass) {
|
||||
constructor(testClass: Class<*>, val ide: Ide?) : BlockJUnit4ClassRunner(testClass) {
|
||||
|
||||
constructor(testClass: Class<*>): this(testClass, null)
|
||||
constructor(testClass: Class<*>, suiteClass: Class<*>, ide: Ide?) : this(testClass, ide) {
|
||||
mySuiteClass = suiteClass
|
||||
}
|
||||
|
||||
constructor(testClass: Class<*>) : this(testClass, null)
|
||||
|
||||
val SERVER_LOG = org.apache.log4j.Logger.getLogger("#com.intellij.testGuiFramework.framework.GuiTestLocalRunner")!!
|
||||
val criticalError = Ref<Boolean>(false)
|
||||
var mySuiteClass: Class<*>? = null
|
||||
|
||||
override fun runChild(method: FrameworkMethod, notifier: RunNotifier) {
|
||||
|
||||
@@ -68,7 +74,9 @@ class GuiTestLocalRunner @Throws(InitializationError::class)
|
||||
|
||||
val description = this@GuiTestLocalRunner.describeChild(method)
|
||||
val eachNotifier = EachTestNotifier(notifier, description)
|
||||
if (criticalError.get()) { eachNotifier.fireTestIgnored(); return }
|
||||
if (criticalError.get()) {
|
||||
eachNotifier.fireTestIgnored(); return
|
||||
}
|
||||
|
||||
SERVER_LOG.info("Starting test on server side: ${testClass.name}#${method.name}")
|
||||
val server = JUnitServerHolder.getServer()
|
||||
@@ -76,7 +84,7 @@ class GuiTestLocalRunner @Throws(InitializationError::class)
|
||||
try {
|
||||
if (!server.isConnected()) {
|
||||
val localIde = ide ?: getIdeFromAnnotation(this@GuiTestLocalRunner.testClass.javaClass)
|
||||
runIdeLocally(port = server.getPort(), ide = localIde)
|
||||
runIde(port = server.getPort(), ide = localIde)
|
||||
if (!server.isStarted())
|
||||
server.start()
|
||||
}
|
||||
@@ -89,15 +97,20 @@ class GuiTestLocalRunner @Throws(InitializationError::class)
|
||||
Assert.fail(e.message)
|
||||
}
|
||||
var testIsRunning = true
|
||||
while(testIsRunning) {
|
||||
while (testIsRunning) {
|
||||
val message = server.receive()
|
||||
if (message.content is JUnitInfo && message.content.testClassAndMethodName == JUnitInfo.getClassAndMethodName(description)) {
|
||||
when (message.content.type) {
|
||||
Type.STARTED -> eachNotifier.fireTestStarted()
|
||||
Type.ASSUMPTION_FAILURE -> eachNotifier.addFailedAssumption((message.content.obj as Failure).exception as AssumptionViolatedException)
|
||||
Type.IGNORED -> { eachNotifier.fireTestIgnored(); testIsRunning = false }
|
||||
Type.ASSUMPTION_FAILURE -> eachNotifier.addFailedAssumption(
|
||||
(message.content.obj as Failure).exception as AssumptionViolatedException)
|
||||
Type.IGNORED -> {
|
||||
eachNotifier.fireTestIgnored(); testIsRunning = false
|
||||
}
|
||||
Type.FAILURE -> eachNotifier.addFailure(message.content.obj as Throwable)
|
||||
Type.FINISHED -> { eachNotifier.fireTestFinished(); testIsRunning = false }
|
||||
Type.FINISHED -> {
|
||||
eachNotifier.fireTestFinished(); testIsRunning = false
|
||||
}
|
||||
else -> throw UnsupportedOperationException("Unable to recognize received from JUnitClient")
|
||||
}
|
||||
}
|
||||
@@ -124,7 +137,7 @@ class GuiTestLocalRunner @Throws(InitializationError::class)
|
||||
server.stopServer()
|
||||
//start a new one IDE
|
||||
val localIde = ide ?: getIdeFromAnnotation(this@GuiTestLocalRunner.testClass.javaClass)
|
||||
runIdeLocally(port = server.getPort(), ide = localIde)
|
||||
runIde(port = server.getPort(), ide = localIde)
|
||||
server.start()
|
||||
}
|
||||
|
||||
@@ -135,7 +148,7 @@ class GuiTestLocalRunner @Throws(InitializationError::class)
|
||||
}
|
||||
|
||||
private fun sendResumeTestCommand(method: FrameworkMethod,
|
||||
server: JUnitServer, resumeTestLabel: String) {
|
||||
server: JUnitServer, resumeTestLabel: String) {
|
||||
val jUnitTestContainer = JUnitTestContainer(method.declaringClass, method.name, additionalInfo = resumeTestLabel)
|
||||
server.send(TransportMessage(MessageType.RESUME_TEST, jUnitTestContainer))
|
||||
}
|
||||
@@ -167,28 +180,54 @@ class GuiTestLocalRunner @Throws(InitializationError::class)
|
||||
LOG.info("Starting test: '${testClass.name}.${method.name}'")
|
||||
//if IDE has a fatal errors from a previous test
|
||||
if (GuiTestUtilKt.fatalErrorsFromIde().isNotEmpty() or GuiTestUtil.doesIdeHaveFatalErrors()) {
|
||||
val restartIdeMessage = TransportMessage(MessageType.RESTART_IDE, "IDE has fatal errors from previous test, let's start a new instance")
|
||||
val restartIdeMessage = TransportMessage(MessageType.RESTART_IDE,
|
||||
"IDE has fatal errors from previous test, let's start a new instance")
|
||||
GuiTestThread.client?.send(restartIdeMessage) ?: throw Exception("JUnitClient is accidentally null")
|
||||
}
|
||||
else {
|
||||
if (!GuiTestStarter.isGuiTestThread())
|
||||
runIdeLocally()
|
||||
runIdeLocally() //TODO: investigate this case
|
||||
else
|
||||
super.runChild(method, notifier)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
catch (e: Exception) {
|
||||
LOG.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun runIde(port: Int, ide: Ide) {
|
||||
val testClassNames = getTestClassesNames()
|
||||
if (testClassNames.isEmpty()) throw Exception("Test classes are not declared.")
|
||||
runIdeLocally(port = port,
|
||||
ide = ide,
|
||||
testClassNames = testClassNames)
|
||||
}
|
||||
|
||||
private fun getTestClassesNames(): List<String> {
|
||||
if (mySuiteClass != null) {
|
||||
val annotation = mySuiteClass!!.getAnnotation(Suite.SuiteClasses::class.java)
|
||||
if (annotation?.value !is Array<*>) throw Exception("Annotation @Suite.SuiteClasses for suite doesn't contain classes as value or is null")
|
||||
val array = annotation.value
|
||||
return array.map {
|
||||
if (it !is KClass<*>) throw Exception("Annotation @Suite.SuiteClasses for suite contains something else from Class<*> type")
|
||||
it.java.canonicalName
|
||||
}
|
||||
}
|
||||
else {
|
||||
return listOf(this@GuiTestLocalRunner.testClass.javaClass.canonicalName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private val LOG = Logger.getInstance("#com.intellij.testGuiFramework.framework.GuiTestRunner")
|
||||
|
||||
fun getIdeFromAnnotation(clazz: Class<*>): Ide {
|
||||
val annotation = clazz.getAnnotation(RunWithIde::class.java)
|
||||
val value = annotation?.value
|
||||
val ideType = if(value != null) (value as KClass<out IdeType>).java.newInstance() else CommunityIde()
|
||||
val ideType = if (value != null) (value as KClass<out IdeType>).java.newInstance() else CommunityIde()
|
||||
return Ide(ideType, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ import org.junit.runner.notification.RunNotifier
|
||||
import org.junit.runners.Suite
|
||||
import org.junit.runners.model.RunnerBuilder
|
||||
|
||||
class GuiTestSuite(suiteClass: Class<*>, val builder: RunnerBuilder) : Suite(suiteClass, builder) {
|
||||
class GuiTestSuite(val suiteClass: Class<*>, val builder: RunnerBuilder) : Suite(suiteClass, builder) {
|
||||
|
||||
//IDE type to run suite tests with
|
||||
val myIde = GuiTestLocalRunner.getIdeFromAnnotation(suiteClass)
|
||||
@@ -40,7 +40,7 @@ class GuiTestSuite(suiteClass: Class<*>, val builder: RunnerBuilder) : Suite(sui
|
||||
//let's start IDE to complete installation, import configs and etc before running tests
|
||||
if (myFirstStart) firstStart()
|
||||
val testClass = runner.description.testClass
|
||||
val guiTestLocalRunner = GuiTestLocalRunner(testClass, myIde)
|
||||
val guiTestLocalRunner = GuiTestLocalRunner(testClass, suiteClass, myIde)
|
||||
super.runChild(guiTestLocalRunner, notifier)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
|
||||
+85
-18
@@ -21,7 +21,6 @@ import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.testGuiFramework.impl.GuiTestStarter
|
||||
import com.intellij.testGuiFramework.launcher.classpath.ClassPathBuilder
|
||||
import com.intellij.testGuiFramework.launcher.classpath.ClassPathBuilder.Companion.isWin
|
||||
import com.intellij.testGuiFramework.launcher.classpath.PathUtils
|
||||
import com.intellij.testGuiFramework.launcher.ide.CommunityIde
|
||||
import com.intellij.testGuiFramework.launcher.ide.Ide
|
||||
@@ -47,7 +46,6 @@ import java.util.stream.Collectors
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @author Sergey Karashevich
|
||||
*/
|
||||
@@ -84,9 +82,9 @@ object GuiTestLocalLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
fun runIdeLocally(ide: Ide = Ide(CommunityIde(), 0, 0), port: Int = 0) {
|
||||
fun runIdeLocally(ide: Ide = Ide(CommunityIde(), 0, 0), port: Int = 0, testClassNames: List<String> = emptyList()) {
|
||||
//todo: check that we are going to run test locally
|
||||
val args = createArgs(ide = ide, port = port)
|
||||
val args = createArgs(ide = ide, port = port, testClassNames = testClassNames)
|
||||
return startIde(ide = ide, args = args)
|
||||
}
|
||||
|
||||
@@ -145,31 +143,38 @@ object GuiTestLocalLauncher {
|
||||
= startIde(ide = ide, needToWait = true, timeOut = 180, args = args)
|
||||
|
||||
|
||||
private fun createArgs(ide: Ide, mainClass: String = "com.intellij.idea.Main", port: Int = 0): List<String>
|
||||
private fun createArgs(ide: Ide, mainClass: String = "com.intellij.idea.Main", port: Int = 0, testClassNames: List<String>): List<String>
|
||||
= createArgsBase(ide = ide,
|
||||
mainClass = mainClass,
|
||||
commandName = GuiTestStarter.COMMAND_NAME,
|
||||
port = port)
|
||||
port = port,
|
||||
testClassNames = testClassNames)
|
||||
|
||||
private fun createArgsForFirstStart(ide: Ide, firstStartClassName: String = "undefined", port: Int = 0): List<String>
|
||||
= createArgsBase(ide = ide,
|
||||
mainClass = "com.intellij.testGuiFramework.impl.FirstStarterKt",
|
||||
firstStartClassName = firstStartClassName,
|
||||
commandName = null,
|
||||
port = port)
|
||||
port = port,
|
||||
testClassNames = emptyList())
|
||||
|
||||
/**
|
||||
* customVmOptions should contain a full VM options formatted items like: customVmOptions = listOf("-Dapple.laf.useScreenMenuBar=true", "-Dide.mac.file.chooser.native=false").
|
||||
* GuiTestLocalLauncher passed all VM options from test, that starts with "-Dpass."
|
||||
*/
|
||||
private fun createArgsBase(ide: Ide, mainClass: String, commandName: String?, firstStartClassName: String = "undefined", port: Int): List<String> {
|
||||
private fun createArgsBase(ide: Ide,
|
||||
mainClass: String,
|
||||
commandName: String?,
|
||||
firstStartClassName: String = "undefined",
|
||||
port: Int,
|
||||
testClassNames: List<String>): List<String> {
|
||||
val customVmOptions = getCustomPassedOptions()
|
||||
var resultingArgs = listOf<String>()
|
||||
.plus(getCurrentJavaExec())
|
||||
.plus(getDefaultAndCustomVmOptions(ide, customVmOptions))
|
||||
.plus("-Didea.gui.test.first.start.class=$firstStartClassName")
|
||||
.plus("-classpath")
|
||||
.plus(getOsSpecificClasspath(ide.ideType.mainModule))
|
||||
.plus(getOsSpecificClasspath(ide.ideType.mainModule, testClassNames))
|
||||
.plus(mainClass)
|
||||
|
||||
if (commandName != null) resultingArgs = resultingArgs.plus(commandName)
|
||||
@@ -236,27 +241,61 @@ object GuiTestLocalLauncher {
|
||||
return PathUtils.getJreBinPath()
|
||||
}
|
||||
|
||||
private fun getOsSpecificClasspath(moduleName: String): String = ClassPathBuilder.buildOsSpecific(
|
||||
getFullClasspath(moduleName).map { it.path })
|
||||
private fun getOsSpecificClasspath(moduleName: String, testClassNames: List<String>): String = ClassPathBuilder.buildOsSpecific(
|
||||
getFullClasspath(moduleName, testClassNames).map { it.path })
|
||||
|
||||
|
||||
/**
|
||||
* return union of classpaths for current test (get from classloader) and classpaths of main and testGuiFramework modules*
|
||||
*/
|
||||
private fun getFullClasspath(moduleName: String): List<File> {
|
||||
val classpath = getExtendedClasspath(moduleName)
|
||||
classpath.addAll(getTestClasspath())
|
||||
private fun getFullClasspath(moduleName: String, testClassNames: List<String>): List<File> {
|
||||
val classpath: MutableSet<File> = substituteAllMacro(getExtendedClasspath(moduleName))
|
||||
classpath.addAll(getTestClasspath(testClassNames))
|
||||
return classpath.toList()
|
||||
}
|
||||
|
||||
private fun getTestClasspath(): List<File> {
|
||||
/**
|
||||
* Finds in a current classpath that built from a test module dependencies resolved macro path
|
||||
* macroName = "\$MAVEN_REPOSITORY\$"
|
||||
*/
|
||||
private fun resolveMacro(classpath: MutableSet<File>, macroName: String): String {
|
||||
val pathWithMacro = classpath.firstOrNull { it.startsWith(macroName) }?.path ?: throw Exception(
|
||||
"Unable to find file in a classpath starting with next macro: '$macroName'")
|
||||
val tailOfPathWithMacro = pathWithMacro.substring(macroName.length)
|
||||
val urlPaths = getUrlPathsFromClassloader()
|
||||
val fullPathWithResolvedMacro = urlPaths.firstOrNull { it.endsWith(tailOfPathWithMacro) } ?: throw Exception(
|
||||
"Unable to find in classpath URL with the next tail: $tailOfPathWithMacro")
|
||||
return fullPathWithResolvedMacro.substring(0..(fullPathWithResolvedMacro.length - tailOfPathWithMacro.length))
|
||||
}
|
||||
|
||||
private fun substituteAllMacro(classpath: MutableSet<File>): MutableSet<File> {
|
||||
val macroList = listOf("\$MAVEN_REPOSITORY\$", "\$KOTLIN_BUNDLED\$")
|
||||
val macroMap = mutableMapOf<String, String>()
|
||||
macroList.forEach { macroMap.put(it, resolveMacro(classpath, it)) }
|
||||
val mutableClasspath = mutableListOf<File>()
|
||||
classpath.forEachIndexed { index, file ->
|
||||
val macro = file.path.findStartsWith(macroList)
|
||||
if (macro != null) {
|
||||
val resolvedMacro = macroMap.get(macro)
|
||||
val newPath = resolvedMacro + file.path.substring(macro.length + 1)
|
||||
mutableClasspath.add(File(newPath))
|
||||
} else mutableClasspath.add(file)
|
||||
}
|
||||
return mutableClasspath.toMutableSet()
|
||||
}
|
||||
|
||||
private fun String.findStartsWith(list: List<String>): String? {
|
||||
return list.find { this.startsWith(it) }
|
||||
}
|
||||
|
||||
private fun getUrlPathsFromClassloader(): List<String> {
|
||||
val classLoader = this.javaClass.classLoader
|
||||
val urlClassLoaderClass = classLoader.javaClass
|
||||
val getUrlsMethod = urlClassLoaderClass.methods.firstOrNull { it.name.toLowerCase() == "geturls" }!!
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val urlsListOrArray = getUrlsMethod.invoke(classLoader)
|
||||
var urls = (urlsListOrArray as? List<*> ?: (urlsListOrArray as Array<*>).toList()).filterIsInstance(URL::class.java)
|
||||
if (isWin()) {
|
||||
if (SystemInfo.isWin()) {
|
||||
val classPathUrl = urls.find { it.toString().contains(Regex("classpath[\\d]*.jar")) }
|
||||
if (classPathUrl != null) {
|
||||
val jarStream = JarInputStream(File(classPathUrl.path).inputStream())
|
||||
@@ -264,7 +303,36 @@ object GuiTestLocalLauncher {
|
||||
urls = mf.mainAttributes.getValue("Class-Path").split(" ").map { URL(it) }
|
||||
}
|
||||
}
|
||||
return urls.map { Paths.get(it.toURI()).toFile() }
|
||||
return urls.map { Paths.get(it.toURI()).toFile().path }
|
||||
}
|
||||
|
||||
private fun getTestClasspath(testClassNames: List<String>): List<File> {
|
||||
if (testClassNames.isEmpty()) return emptyList()
|
||||
val fileSet = mutableSetOf<File>()
|
||||
testClassNames.forEach {
|
||||
fileSet.add(getClassFile(it))
|
||||
}
|
||||
return fileSet.toList()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* returns a file (directory or a jar) containing class loaded by a class loader with a given name
|
||||
*/
|
||||
private fun getClassFile(className: String): File {
|
||||
val classLoader = this.javaClass.classLoader
|
||||
val cls = classLoader.loadClass(className) ?: throw Exception(
|
||||
"Unable to load class ($className) with a given classloader. Check the path to class or a classloader URLs.")
|
||||
val name = "${cls.simpleName}.class"
|
||||
val packagePath = cls.`package`.name.replace(".", "/")
|
||||
val fullPath = "$packagePath/$name"
|
||||
val resourceUrl = classLoader.getResource(fullPath) ?: throw Exception(
|
||||
"Unable to get resource path to a \"$fullPath\". Check the path to class or a classloader URLs.")
|
||||
var cutPath = resourceUrl.path.substring(0, resourceUrl.path.length - fullPath.length)
|
||||
if (cutPath.endsWith("!") or cutPath.endsWith("!/")) cutPath = cutPath.substring(0..(cutPath.length - 3)) // in case of it is a jar
|
||||
val file = File(cutPath)
|
||||
if (!file.exists()) throw Exception("File for a class '$className' doesn't exist by path: $cutPath")
|
||||
return file
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +345,6 @@ object GuiTestLocalLauncher {
|
||||
val resultSet = LinkedHashSet<File>()
|
||||
val module = modulesList.module(moduleName) ?: throw Exception("Unable to find module with name: $moduleName")
|
||||
resultSet.addAll(module.getClasspath())
|
||||
resultSet.addAll(testGuiFrameworkModule.getClasspath())
|
||||
return resultSet
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user