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 }