From 4f647e84c947a05d40609e46135b96472f5a72f3 Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Thu, 17 Aug 2017 16:28:27 +0300 Subject: [PATCH] Line Marking: Respect @Test/@Ignore annotations in test line markers #KT-16814 In Progress --- .../jetbrains/kotlin/idea/util/stringUtil.kt | 30 ++++++ .../KotlinTestRunLineMarkerContributor.kt | 92 ++++++++++++++++--- .../org/jetbrains/kotlin/idea/js/jsUtils.kt | 32 +++++++ .../kotlinTestAnnotations/common/common.kt | 6 ++ .../kotlinTestAnnotations/js/test.kt | 18 ++++ .../resolve/MultiModuleLineMarkerTest.kt | 14 +++ 6 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/js/jsUtils.kt create mode 100644 idea/testData/multiModuleLineMarker/kotlinTestAnnotations/common/common.kt create mode 100644 idea/testData/multiModuleLineMarker/kotlinTestAnnotations/js/test.kt diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/util/stringUtil.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/util/stringUtil.kt index bb7a738453c..1edac055985 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/util/stringUtil.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/util/stringUtil.kt @@ -33,3 +33,33 @@ fun String.collapseSpaces(): String { } return builder.toString() } + +// -------------------- copied from EscapeUtils.java -------------------- + +private val ESCAPE_CHAR = '\\' + +private fun calcExpectedJoinedSize(list: Collection) = list.size - 1 + list.sumBy { it.length } + +fun Collection.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() +} + +// ---------------------------------------------------------------------- \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinTestRunLineMarkerContributor.kt b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinTestRunLineMarkerContributor.kt index 0a3688407d6..db05abf0922 100644 --- a/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinTestRunLineMarkerContributor.kt +++ b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinTestRunLineMarkerContributor.kt @@ -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() ?: 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() + + locations += FileUtil.toSystemDependentName(testFilePath) + + val klass = when (declaration) { + is KtClassOrObject -> declaration + is KtNamedFunction -> declaration.containingClassOrObject ?: return null + else -> return null + } + locations += klass.parentsWithSelf.filterIsInstance().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() ?: 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()) } } diff --git a/idea/src/org/jetbrains/kotlin/idea/js/jsUtils.kt b/idea/src/org/jetbrains/kotlin/idea/js/jsUtils.kt new file mode 100644 index 00000000000..05635873ac2 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/js/jsUtils.kt @@ -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 "" \ No newline at end of file diff --git a/idea/testData/multiModuleLineMarker/kotlinTestAnnotations/common/common.kt b/idea/testData/multiModuleLineMarker/kotlinTestAnnotations/common/common.kt new file mode 100644 index 00000000000..3550e004b80 --- /dev/null +++ b/idea/testData/multiModuleLineMarker/kotlinTestAnnotations/common/common.kt @@ -0,0 +1,6 @@ +// !CHECK_HIGHLIGHTING + +package kotlin.test + +annotation class Test +annotation class Ignore \ No newline at end of file diff --git a/idea/testData/multiModuleLineMarker/kotlinTestAnnotations/js/test.kt b/idea/testData/multiModuleLineMarker/kotlinTestAnnotations/js/test.kt new file mode 100644 index 00000000000..9ef8f426ebd --- /dev/null +++ b/idea/testData/multiModuleLineMarker/kotlinTestAnnotations/js/test.kt @@ -0,0 +1,18 @@ +import kotlin.test.* + +class SimpleTest { + @Test fun testFoo() { + // Will run + } + + @Ignore fun testFooWrong() { + // Will not run + } +} + +@Ignore +class TestTest { + @Test fun emptyTest() { + // Will not run + } +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/caches/resolve/MultiModuleLineMarkerTest.kt b/idea/tests/org/jetbrains/kotlin/idea/caches/resolve/MultiModuleLineMarkerTest.kt index 6d186a24016..5123127117a 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/caches/resolve/MultiModuleLineMarkerTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/caches/resolve/MultiModuleLineMarkerTest.kt @@ -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") + } + } + }) + } } \ No newline at end of file