Line Marking: Respect @Test/@Ignore annotations in test line markers

#KT-16814 In Progress
This commit is contained in:
Alexey Sedunov
2017-08-17 16:28:27 +03:00
parent 5a5a27b983
commit 4f647e84c9
6 changed files with 179 additions and 13 deletions
@@ -33,3 +33,33 @@ fun String.collapseSpaces(): String {
}
return builder.toString()
}
// -------------------- copied from EscapeUtils.java --------------------
private val ESCAPE_CHAR = '\\'
private fun calcExpectedJoinedSize(list: Collection<String>) = list.size - 1 + list.sumBy { it.length }
fun Collection<String>.joinWithEscape(delimiterChar: Char): String {
if (isEmpty()) return ""
val expectedSize = calcExpectedJoinedSize(this)
val out = StringBuilder(expectedSize)
var first = true
for (s in this) {
if (!first) {
out.append(delimiterChar)
}
first = false
for (i in 0 until s.length) {
val ch = s[i]
if (ch == delimiterChar || ch == ESCAPE_CHAR) {
out.append(ESCAPE_CHAR)
}
out.append(ch)
}
}
return out.toString()
}
// ----------------------------------------------------------------------
@@ -23,19 +23,37 @@ import com.intellij.execution.lineMarker.RunLineMarkerContributor
import com.intellij.execution.testframework.TestIconMapper
import com.intellij.execution.testframework.sm.runner.states.TestStateInfo
import com.intellij.icons.AllIcons
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.asJava.toLightMethods
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ClassDescriptorWithResolutionScopes
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.js.getJsOutputFilePath
import org.jetbrains.kotlin.idea.project.TargetPlatformDetector
import org.jetbrains.kotlin.idea.util.string.joinWithEscape
import org.jetbrains.kotlin.js.resolve.JsPlatform
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform
import javax.swing.Icon
class KotlinTestRunLineMarkerContributor : RunLineMarkerContributor() {
companion object {
private val TEST_FQ_NAME = FqName("kotlin.test.Test")
private val IGNORE_FQ_NAME = FqName("kotlin.test.Ignore")
}
private fun getTestStateIcon(url: String, project: Project): Icon? {
val defaultIcon = AllIcons.RunConfigurations.TestState.Run
val state = TestStateStorage.getInstance(project).getState(url) ?: return defaultIcon
@@ -49,17 +67,7 @@ class KotlinTestRunLineMarkerContributor : RunLineMarkerContributor() {
}
}
override fun getInfo(element: PsiElement): RunLineMarkerContributor.Info? {
val declaration = element.getStrictParentOfType<KtNamedDeclaration>() ?: return null
if (declaration.nameIdentifier != element) return null
if (declaration !is KtClassOrObject && declaration !is KtNamedFunction) return null
// To prevent IDEA failing on red code
if (declaration.resolveToDescriptorIfAny() == null) return null
val project = element.project
private fun getJavaTestIcon(declaration: KtNamedDeclaration): Icon? {
val (url, framework) = when (declaration) {
is KtClassOrObject -> {
val lightClass = declaration.toLightClass() ?: return null
@@ -81,8 +89,66 @@ class KotlinTestRunLineMarkerContributor : RunLineMarkerContributor() {
else -> return null
}
return getTestStateIcon(url, declaration.project) ?: framework.icon
}
val icon = getTestStateIcon(url, project) ?: framework.icon
return RunLineMarkerContributor.Info(icon, { "Run Test" }, ExecutorAction.getActions(1))
private fun DeclarationDescriptor.isIgnored(): Boolean =
annotations.any { it.fqName == IGNORE_FQ_NAME } || ((containingDeclaration as? ClassDescriptor)?.isIgnored() ?: false)
private fun DeclarationDescriptor.isTest(): Boolean {
if (isIgnored()) return false
if (annotations.any { it.fqName == TEST_FQ_NAME }) return true
if (this is ClassDescriptorWithResolutionScopes) {
return declaredCallableMembers.any { it.isTest() }
}
return false
}
private fun getJavaScriptTestIcon(declaration: KtNamedDeclaration, descriptor: DeclarationDescriptor): Icon? {
if (!descriptor.isTest()) return null
val module = ModuleUtilCore.findModuleForPsiElement(declaration) ?: return null
val testFilePath = getJsOutputFilePath(module, true, false) ?: return null
val locations = ArrayList<String>()
locations += FileUtil.toSystemDependentName(testFilePath)
val klass = when (declaration) {
is KtClassOrObject -> declaration
is KtNamedFunction -> declaration.containingClassOrObject ?: return null
else -> return null
}
locations += klass.parentsWithSelf.filterIsInstance<KtNamedDeclaration>().mapNotNull { it.name }.toList().asReversed()
val testName = (declaration as? KtNamedFunction)?.name
if (testName != null) {
locations += "$testName"
}
val prefix = if (testName != null) "test://" else "suite://"
val url = prefix + locations.joinWithEscape('.')
return getTestStateIcon(url, declaration.project)
}
override fun getInfo(element: PsiElement): RunLineMarkerContributor.Info? {
val declaration = element.getStrictParentOfType<KtNamedDeclaration>() ?: return null
if (declaration.nameIdentifier != element) return null
if (declaration !is KtClassOrObject && declaration !is KtNamedFunction) return null
// To prevent IDEA failing on red code
val descriptor = declaration.resolveToDescriptorIfAny() ?: return null
val platform = TargetPlatformDetector.getPlatform(declaration.containingKtFile)
val icon = when (platform) {
is JvmPlatform -> getJavaTestIcon(declaration)
is JsPlatform -> getJavaScriptTestIcon(declaration, descriptor)
else -> return null
}
return RunLineMarkerContributor.Info(icon, { "Run Test" }, ExecutorAction.getActions())
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.js
import com.intellij.openapi.module.Module
import com.intellij.openapi.roots.CompilerModuleExtension
import org.jetbrains.jps.util.JpsPathUtil
import org.jetbrains.kotlin.utils.KotlinJavascriptMetadataUtils
fun getJsOutputFilePath(module: Module, isTests: Boolean, isMeta: Boolean): String? {
val compilerExtension = CompilerModuleExtension.getInstance(module)
val outputDir = (if (isTests) compilerExtension?.compilerOutputUrlForTests else compilerExtension?.compilerOutputUrl)
?: return null
val extension = if (isMeta) KotlinJavascriptMetadataUtils.META_JS_SUFFIX else KotlinJavascriptMetadataUtils.JS_EXT
return JpsPathUtil.urlToPath("$outputDir/${module.name}${suffix(isTests)}$extension")
}
private fun suffix(isTests: Boolean) = if (isTests) "_test" else ""
@@ -0,0 +1,6 @@
// !CHECK_HIGHLIGHTING
package kotlin.test
annotation class Test
annotation class Ignore
@@ -0,0 +1,18 @@
import kotlin.test.*
class <lineMarker descr="Run Test">SimpleTest</lineMarker> {
@Test fun <lineMarker descr="Run Test">testFoo</lineMarker>() {
// Will run
}
@Ignore fun testFooWrong() {
// Will not run
}
}
@Ignore
class TestTest {
@Test fun emptyTest() {
// Will not run
}
}
@@ -16,6 +16,8 @@
package org.jetbrains.kotlin.idea.caches.resolve
import com.intellij.openapi.roots.CompilerModuleExtension
import com.intellij.openapi.roots.ModuleRootModificationUtil
import org.jetbrains.kotlin.config.JvmTarget
import org.jetbrains.kotlin.config.TargetPlatformKind
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
@@ -49,4 +51,16 @@ class MultiModuleLineMarkerTest : AbstractMultiModuleHighlightingTest() {
fun testSuspendImplInPlatformModules() {
doMultiPlatformTest(TargetPlatformKind.Jvm[JvmTarget.JVM_1_6], TargetPlatformKind.JavaScript)
}
fun testKotlinTestAnnotations() {
doMultiPlatformTest(TargetPlatformKind.JavaScript,
configureModule = { module, _ ->
ModuleRootModificationUtil.updateModel(module) {
with(it.getModuleExtension(CompilerModuleExtension::class.java)!!) {
inheritCompilerOutputPath(false)
setCompilerOutputPathForTests("js_out")
}
}
})
}
}