From c7ac73672dc3f005bae48784d85eb977c779ef1b Mon Sep 17 00:00:00 2001 From: Sergey Karashevich Date: Tue, 17 Oct 2017 20:57:30 +0300 Subject: [PATCH] [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. --- .../framework/GuiTestLocalRunner.kt | 67 +++++++++--- .../framework/GuiTestSuite.kt | 4 +- .../launcher/GuiTestLocalLauncher.kt | 103 +++++++++++++++--- 3 files changed, 140 insertions(+), 34 deletions(-) diff --git a/platform/testGuiFramework/src/com/intellij/testGuiFramework/framework/GuiTestLocalRunner.kt b/platform/testGuiFramework/src/com/intellij/testGuiFramework/framework/GuiTestLocalRunner.kt index 99b50e95df98..c54189346633 100755 --- a/platform/testGuiFramework/src/com/intellij/testGuiFramework/framework/GuiTestLocalRunner.kt +++ b/platform/testGuiFramework/src/com/intellij/testGuiFramework/framework/GuiTestLocalRunner.kt @@ -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(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 { + 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).java.newInstance() else CommunityIde() + val ideType = if (value != null) (value as KClass).java.newInstance() else CommunityIde() return Ide(ideType, 0, 0) } } diff --git a/platform/testGuiFramework/src/com/intellij/testGuiFramework/framework/GuiTestSuite.kt b/platform/testGuiFramework/src/com/intellij/testGuiFramework/framework/GuiTestSuite.kt index ec90f2ee64d9..519a97b2d327 100644 --- a/platform/testGuiFramework/src/com/intellij/testGuiFramework/framework/GuiTestSuite.kt +++ b/platform/testGuiFramework/src/com/intellij/testGuiFramework/framework/GuiTestSuite.kt @@ -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) { diff --git a/platform/testGuiFramework/src/com/intellij/testGuiFramework/launcher/GuiTestLocalLauncher.kt b/platform/testGuiFramework/src/com/intellij/testGuiFramework/launcher/GuiTestLocalLauncher.kt index 7c96dd7e1313..c29a6857a58b 100644 --- a/platform/testGuiFramework/src/com/intellij/testGuiFramework/launcher/GuiTestLocalLauncher.kt +++ b/platform/testGuiFramework/src/com/intellij/testGuiFramework/launcher/GuiTestLocalLauncher.kt @@ -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 = 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 + private fun createArgs(ide: Ide, mainClass: String = "com.intellij.idea.Main", port: Int = 0, testClassNames: List): List = 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 = 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 { + private fun createArgsBase(ide: Ide, + mainClass: String, + commandName: String?, + firstStartClassName: String = "undefined", + port: Int, + testClassNames: List): List { val customVmOptions = getCustomPassedOptions() var resultingArgs = listOf() .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 = 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 { - val classpath = getExtendedClasspath(moduleName) - classpath.addAll(getTestClasspath()) + private fun getFullClasspath(moduleName: String, testClassNames: List): List { + val classpath: MutableSet = substituteAllMacro(getExtendedClasspath(moduleName)) + classpath.addAll(getTestClasspath(testClassNames)) return classpath.toList() } - private fun getTestClasspath(): List { + /** + * Finds in a current classpath that built from a test module dependencies resolved macro path + * macroName = "\$MAVEN_REPOSITORY\$" + */ + private fun resolveMacro(classpath: MutableSet, 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): MutableSet { + val macroList = listOf("\$MAVEN_REPOSITORY\$", "\$KOTLIN_BUNDLED\$") + val macroMap = mutableMapOf() + macroList.forEach { macroMap.put(it, resolveMacro(classpath, it)) } + val mutableClasspath = mutableListOf() + 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? { + return list.find { this.startsWith(it) } + } + + private fun getUrlPathsFromClassloader(): List { 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): List { + if (testClassNames.isEmpty()) return emptyList() + val fileSet = mutableSetOf() + 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() val module = modulesList.module(moduleName) ?: throw Exception("Unable to find module with name: $moduleName") resultSet.addAll(module.getClasspath()) - resultSet.addAll(testGuiFrameworkModule.getClasspath()) return resultSet }