Test local inspections on the fly via highlighting

Fixes problem with INFORMATION highlight type in batch mode (IDEA 181).
It's no more required to report INFORMATIONs in unit tests in batch.
This commit is contained in:
Mikhail Glukhikh
2018-04-17 20:11:23 +03:00
parent b6661710dd
commit ef3ba5f0b1
11 changed files with 122 additions and 112 deletions
@@ -66,8 +66,7 @@ abstract class AbstractKotlinInspection: LocalInspectionTool(), CustomSuppressab
range: TextRange?, range: TextRange?,
vararg fixes: LocalQuickFix vararg fixes: LocalQuickFix
) { ) {
if (!ApplicationManager.getApplication().isUnitTestMode && if (!isOnTheFly && highlightType == ProblemHighlightType.INFORMATION) return
!isOnTheFly && highlightType == ProblemHighlightType.INFORMATION) return
val problemDescriptor = manager.createProblemDescriptor(element, range, description, highlightType, isOnTheFly, *fixes) val problemDescriptor = manager.createProblemDescriptor(element, range, description, highlightType, isOnTheFly, *fixes)
registerProblem(problemDescriptor) registerProblem(problemDescriptor)
} }
@@ -31,21 +31,5 @@
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Equality check can be used instead of elvis for nullable boolean check</problem_class> <problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Equality check can be used instead of elvis for nullable boolean check</problem_class>
<description>Equality check should be used instead of elvis for nullable boolean check</description> <description>Equality check should be used instead of elvis for nullable boolean check</description>
</problem> </problem>
<problem>
<file>test.kt</file>
<line>13</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="test.kt"/>
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Equality check can be used instead of elvis for nullable boolean check</problem_class>
<description>Equality check can be used instead of elvis for nullable boolean check</description>
</problem>
<problem>
<file>test.kt</file>
<line>14</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="test.kt"/>
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Equality check can be used instead of elvis for nullable boolean check</problem_class>
<description>Equality check can be used instead of elvis for nullable boolean check</description>
</problem>
</problems> </problems>
+2 -2
View File
@@ -10,6 +10,6 @@ fun foo() {
if (a ?: false || !(b ?: true)) { if (a ?: false || !(b ?: true)) {
} }
val x = a ?: false val x = a ?: false // INFORMATION -- not reported in batch
val y = !(b ?: true) val y = !(b ?: true) // INFORMATION -- not reported in batch
} }
@@ -31,20 +31,4 @@
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Can be replaced with binary operator</problem_class> <problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Can be replaced with binary operator</problem_class>
<description>Call replaceable with binary operator</description> <description>Call replaceable with binary operator</description>
</problem> </problem>
<problem>
<file>test.kt</file>
<line>8</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="temp:///src/test.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Can be replaced with binary operator</problem_class>
<description>Call replaceable with binary operator</description>
</problem>
<problem>
<file>test.kt</file>
<line>9</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="temp:///src/test.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Can be replaced with binary operator</problem_class>
<description>Call replaceable with binary operator</description>
</problem>
</problems> </problems>
@@ -5,6 +5,6 @@ fun foo() {
1.compareTo(1) == 0 // NO 1.compareTo(1) == 0 // NO
2.compareTo(1) > 0 // YES 2.compareTo(1) > 0 // YES
0 >= 1.compareTo(2) // YES 0 >= 1.compareTo(2) // YES
2.plus(2) // YES (information) 2.plus(2) // YES (information) -- not reported in batch
2.times(2) // YES (information) 2.times(2) // YES (information) -- not reported in batch
} }
@@ -1,3 +1,5 @@
// HIGHLIGHT: INFORMATION
fun foo() { fun foo() {
var a: Boolean? = null var a: Boolean? = null
val x = a<caret> ?: false val x = a<caret> ?: false
@@ -1,3 +1,5 @@
// HIGHLIGHT: INFORMATION
fun foo() { fun foo() {
var a: Boolean? = null var a: Boolean? = null
val x = a == true val x = a == true
@@ -1,3 +1,5 @@
// HIGHLIGHT: INFORMATION
fun foo() { fun foo() {
var b: Boolean? = null var b: Boolean? = null
val x = !(b <caret>?: true) val x = !(b <caret>?: true)
@@ -1,3 +1,5 @@
// HIGHLIGHT: INFORMATION
fun foo() { fun foo() {
var b: Boolean? = null var b: Boolean? = null
val x = b == false val x = b == false
@@ -17,11 +17,15 @@
package org.jetbrains.kotlin.idea.inspections package org.jetbrains.kotlin.idea.inspections
import com.google.common.collect.Lists import com.google.common.collect.Lists
import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeHighlighting.HighlightDisplayLevel
import com.intellij.codeHighlighting.Pass
import com.intellij.codeInsight.daemon.impl.HighlightInfoType
import com.intellij.codeInsight.intention.EmptyIntentionAction
import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile import com.intellij.profile.codeInspection.ProjectInspectionProfileManager
import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl
import junit.framework.ComparisonFailure import junit.framework.ComparisonFailure
import junit.framework.TestCase import junit.framework.TestCase
import org.jetbrains.kotlin.idea.test.DirectiveBasedActionUtils import org.jetbrains.kotlin.idea.test.DirectiveBasedActionUtils
@@ -29,7 +33,6 @@ import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.idea.test.configureCompilerOptions import org.jetbrains.kotlin.idea.test.configureCompilerOptions
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.test.InTextDirectivesUtils import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.jetbrains.kotlin.test.KotlinTestUtils import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.Assert import org.junit.Assert
@@ -64,12 +67,16 @@ abstract class AbstractLocalInspectionTest : KotlinLightCodeInsightFixtureTestCa
} }
if (candidateFiles.isEmpty()) { if (candidateFiles.isEmpty()) {
throw AssertionError(".inspection file is not found for " + testDataFile + throw AssertionError(
"\nAdd it to base directory of test data. It should contain fully-qualified name of inspection class.") ".inspection file is not found for " + testDataFile +
"\nAdd it to base directory of test data. It should contain fully-qualified name of inspection class."
)
} }
if (candidateFiles.size > 1) { if (candidateFiles.size > 1) {
throw AssertionError("Several .inspection files are available for " + testDataFile + throw AssertionError(
"\nPlease remove some of them\n" + candidateFiles) "Several .inspection files are available for " + testDataFile +
"\nPlease remove some of them\n" + candidateFiles
)
} }
val className = FileUtil.loadFile(candidateFiles[0]).trim { it <= ' ' } val className = FileUtil.loadFile(candidateFiles[0]).trim { it <= ' ' }
@@ -106,9 +113,9 @@ abstract class AbstractLocalInspectionTest : KotlinLightCodeInsightFixtureTestCa
break break
} }
val psiFile = myFixture.configureByFiles(*(listOf(mainFile.name) + extraFileNames).toTypedArray()).first() myFixture.configureByFiles(*(listOf(mainFile.name) + extraFileNames).toTypedArray()).first()
doTestFor(mainFile.name, psiFile.virtualFile!!, inspection, fileText) doTestFor(mainFile.name, inspection, fileText)
if (file is KtFile && !InTextDirectivesUtils.isDirectiveDefined(fileText, "// SKIP_ERRORS_AFTER")) { if (file is KtFile && !InTextDirectivesUtils.isDirectiveDefined(fileText, "// SKIP_ERRORS_AFTER")) {
DirectiveBasedActionUtils.checkForUnexpectedErrors(file as KtFile) DirectiveBasedActionUtils.checkForUnexpectedErrors(file as KtFile)
@@ -116,90 +123,118 @@ abstract class AbstractLocalInspectionTest : KotlinLightCodeInsightFixtureTestCa
} }
protected fun runInspectionWithFixesAndCheck( protected fun runInspectionWithFixesAndCheck(
file: VirtualFile, inspection: AbstractKotlinInspection,
inspection: AbstractKotlinInspection, expectedProblemString: String?,
problemExpectedString: String?, expectedHighlightString: String?,
highlightExpectedString: String?, localFixTextString: String?
localFixTextString: String?
): Boolean { ): Boolean {
val problemExpected = problemExpectedString == null || problemExpectedString != "none" val problemExpected = expectedProblemString == null || expectedProblemString != "none"
val presentation = runInspection(inspection, project, listOf(file)) myFixture.enableInspections(inspection::class.java)
val problemDescriptors = presentation.problemDescriptors
.filterIsInstance<ProblemDescriptor>() // Set default level to WARNING to make possible to test DO_NOT_SHOW
.filter { val inspectionProfileManager = ProjectInspectionProfileManager.getInstance(project)
val caretOffset = myFixture.caretOffset val inspectionProfile = inspectionProfileManager.currentProfile
caretOffset in it.textRangeInElement?.shiftRight(it.psiElement.startOffset) ?: it.psiElement.textRange val state = inspectionProfile.getToolDefaultState(inspection.shortName, project)
} state.level = HighlightDisplayLevel.WARNING
val caretOffset = myFixture.caretOffset
val highlightInfos = CodeInsightTestFixtureImpl.instantiateAndRun(
file, editor, intArrayOf(
Pass.LINE_MARKERS,
Pass.EXTERNAL_TOOLS,
Pass.POPUP_HINTS,
Pass.UPDATE_ALL,
Pass.UPDATE_FOLDING,
Pass.WOLF
), false
).filter { it.description != null && caretOffset in it.startOffset..it.endOffset }
Assert.assertTrue( Assert.assertTrue(
if (!problemExpected) if (!problemExpected)
"No problems should be detected at caret\n" + "No problems should be detected at caret\n" +
"Detected problems: ${problemDescriptors.joinToString { it.descriptionTemplate }}" "Detected problems: ${highlightInfos.joinToString { it.description }}"
else else
"Expected at least one problem at caret", "Expected at least one problem at caret",
problemExpected == problemDescriptors.isNotEmpty()) problemExpected == highlightInfos.isNotEmpty()
if (!problemExpected) return false )
problemDescriptors if (!problemExpected || highlightInfos.isEmpty()) return false
.filter { it.highlightType != ProblemHighlightType.INFORMATION } highlightInfos
.forEach { .filter { it.type != HighlightInfoType.INFORMATION }
Assert.assertTrue("Problem description should not contain 'can': ${it.descriptionTemplate}", .forEach {
" can " !in it.descriptionTemplate) val description = it.description
} Assert.assertTrue(
if (problemExpectedString != null) { "Problem description should not contain 'can': $description",
Assert.assertTrue("Expected the following problem at caret: $problemExpectedString\n" + " can " !in description
"Active problems: ${problemDescriptors.joinToString { it.descriptionTemplate }}", )
problemDescriptors.any { it.descriptionTemplate == problemExpectedString }) }
if (expectedProblemString != null) {
Assert.assertTrue(
"Expected the following problem at caret: $expectedProblemString\n" +
"Active problems: ${highlightInfos.joinToString { it.description }}",
highlightInfos.any { it.description == expectedProblemString }
)
} }
if (highlightExpectedString != null) { val expectedHighlightType = when (expectedHighlightString) {
Assert.assertTrue("Expected the following problem highlight type\n" + null -> null
"Actual types: ${problemDescriptors.joinToString { it.highlightType.toString() } }", ProblemHighlightType.GENERIC_ERROR_OR_WARNING.name -> HighlightDisplayLevel.WARNING.name
problemDescriptors.all { it.highlightType.toString() == highlightExpectedString }) else -> expectedHighlightString
}
if (expectedHighlightType != null) {
Assert.assertTrue(
"Expected the following problem highlight type: $expectedHighlightType\n" +
"Actual type: ${highlightInfos.joinToString { it.type.toString() }}",
highlightInfos.all { expectedHighlightType in it.type.toString() }
)
} }
val allLocalFixActions = problemDescriptors.flatMap { problem -> val allLocalFixActions = highlightInfos.flatMap { it.quickFixActionMarkers }.map { it.first.action }
val fixes = problem.fixes
fixes?.toList() ?: emptyList()
}
val localFixActions = allLocalFixActions.filter { fix -> localFixTextString == null || fix.name == localFixTextString }
val availableDescription = allLocalFixActions.joinToString { it.name }
val fixDescription = localFixTextString?.let { "with specified text '$localFixTextString'"} ?: "" val localFixActions = allLocalFixActions.filter { fix -> localFixTextString == null || fix.text == localFixTextString }
TestCase.assertTrue("No fix action $fixDescription\n" + val availableDescription = allLocalFixActions.joinToString { it.text }
"Available actions: $availableDescription",
localFixActions.isNotEmpty())
val localFixAction = localFixActions.singleOrNull() val fixDescription = localFixTextString?.let { "with specified text '$localFixTextString'" } ?: ""
TestCase.assertTrue("More than one fix action $fixDescription\n" + TestCase.assertTrue(
"Available actions: $availableDescription", "No fix action $fixDescription\n" +
localFixAction != null) "Available actions: $availableDescription",
localFixActions.isNotEmpty()
)
val problemDescriptor = problemDescriptors.find { localFixAction in it.fixes?.toList() ?: emptyList() }!! val localFixAction = localFixActions.singleOrNull { it !is EmptyIntentionAction }
TestCase.assertTrue(
"More than one fix action $fixDescription\n" +
"Available actions: $availableDescription",
localFixAction != null
)
project.executeWriteCommand(localFixAction!!.name, null) { project.executeWriteCommand(localFixAction!!.text, null) {
localFixAction.applyFix(project, problemDescriptor) localFixAction.invoke(project, editor, file)
} }
return true return true
} }
private fun doTestFor(mainFilePath: String, file: VirtualFile, inspection: AbstractKotlinInspection, fileText: String) { private fun doTestFor(mainFilePath: String, inspection: AbstractKotlinInspection, fileText: String) {
val problemExpectedString = InTextDirectivesUtils.findStringWithPrefixes( val expectedProblemString = InTextDirectivesUtils.findStringWithPrefixes(
fileText, "// $expectedProblemDirectiveName: ") fileText, "// $expectedProblemDirectiveName: "
val highlightExpectedString = InTextDirectivesUtils.findStringWithPrefixes( )
fileText, "// $expectedProblemHighlightType: ") val expectedHighlightString = InTextDirectivesUtils.findStringWithPrefixes(
fileText, "// $expectedProblemHighlightType: "
)
val localFixTextString = InTextDirectivesUtils.findStringWithPrefixes( val localFixTextString = InTextDirectivesUtils.findStringWithPrefixes(
fileText, "// $fixTextDirectiveName: ") fileText, "// $fixTextDirectiveName: "
)
if (!runInspectionWithFixesAndCheck(file, inspection, problemExpectedString, highlightExpectedString, localFixTextString)) { if (!runInspectionWithFixesAndCheck(inspection, expectedProblemString, expectedHighlightString, localFixTextString)) {
return return
} }
val canonicalPathToExpectedFile = mainFilePath + afterFileNameSuffix val canonicalPathToExpectedFile = mainFilePath + afterFileNameSuffix
try { try {
myFixture.checkResultByFile(canonicalPathToExpectedFile) myFixture.checkResultByFile(canonicalPathToExpectedFile)
} } catch (e: ComparisonFailure) {
catch (e: ComparisonFailure) {
KotlinTestUtils.assertEqualsToFile( KotlinTestUtils.assertEqualsToFile(
File(testDataPath, canonicalPathToExpectedFile), File(testDataPath, canonicalPathToExpectedFile),
editor.document.text editor.document.text
) )
} }
} }
@@ -58,10 +58,10 @@ abstract class AbstractMultiFileLocalInspectionTest : AbstractLocalInspectionTes
val problemExpectedString = config["problem"]?.asString // null means "some problem", "none" means no problem val problemExpectedString = config["problem"]?.asString // null means "some problem", "none" means no problem
val localFixTextString = config["fix"]?.asString // null means "some single fix" or "none" if no problem expected val localFixTextString = config["fix"]?.asString // null means "some single fix" or "none" if no problem expected
doTest(path) test@ { _ -> doTest(path) test@{ _ ->
val mainFile = myFixture.configureFromTempProjectFile(mainFilePath) myFixture.configureFromTempProjectFile(mainFilePath)
runInspectionWithFixesAndCheck(mainFile.virtualFile, inspection, problemExpectedString, null, localFixTextString) runInspectionWithFixesAndCheck(inspection, problemExpectedString, null, localFixTextString)
} }
} }