[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:
Sergey Karashevich
2017-10-18 11:38:11 +03:00
parent 6035a9d94c
commit c7ac73672d
3 changed files with 140 additions and 34 deletions
@@ -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)
}
}
@@ -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) {
@@ -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
}