[jvm] IDEA-283602 Migrate JUnit 4 converter to UAST

GitOrigin-RevId: f7bfbf867ecb02a04854bcc266146720ac6a8a6a
This commit is contained in:
Bart van Helvert
2022-11-07 15:59:43 +00:00
committed by intellij-monorepo-bot
parent 0db7d61240
commit c18db65d64
13 changed files with 1005 additions and 148 deletions
@@ -52,6 +52,11 @@
groupPathKey="jvm.inspections.group.name" groupKey="jvm.inspections.test.frameworks.group.name"
key="jvm.inspections.junit3.super.teardown.display.name"
implementationClass="com.intellij.codeInspection.test.junit.JUnit3SuperTearDownInspection"/>
<localInspection language="UAST" enabledByDefault="true" level="INFORMATION" shortName="Junit4Converter"
groupBundle="messages.JvmAnalysisBundle" bundle="messages.JvmAnalysisBundle"
groupPathKey="jvm.inspections.group.name" groupKey="jvm.inspections.test.frameworks.group.name"
key="jvm.inspections.junit4.converter.display.name"
implementationClass="com.intellij.codeInspection.test.junit.JUnit4ConverterInspection"/>
<localInspection language="UAST" enabledByDefault="true" level="WARNING" shortName="JUnit5AssertionsConverter"
groupBundle="messages.JvmAnalysisBundle" bundle="messages.JvmAnalysisBundle"
groupPathKey="jvm.inspections.group.name" groupKey="jvm.inspections.test.frameworks.group.name"
@@ -96,6 +96,14 @@ jvm.inspections.test.case.with.constructor.problem.descriptor.initializer=Initia
jvm.inspections.junit3.super.teardown.display.name=JUnit 3 'super.tearDown()' is not called from 'finally' block
jvm.inspections.junit3.super.teardown.problem.descriptor=<code>#ref()</code> is not called from 'finally' block #loc
jvm.inspections.junit4.converter.display.name=JUnit 3 test can be JUnit 4
jvm.inspections.junit4.converter.problem.descriptor=<code>#ref</code> could be converted to JUnit4 test case
jvm.inspections.junit4.converter.quickfix.name=Convert to JUnit 4 test case
jvm.inspections.junit4.converter.quickfix.conflict.semantics=Method call {0} may change semantics when {1} is converted to JUnit 4
jvm.inspections.junit4.converter.quickfix.conflict.suite=Migration of suite method for {0} has side effects which will be deleted
jvm.inspections.junit4.converter.quickfix.conflict.name=Method {0} will have a name collision with its super method
jvm.inspections.junit4.converter.quickfix.conflict.call.compile=Method call {0} will not compile when {1} is converted to JUnit 4
jvm.inspections.junit5.converter.display.name=JUnit 4 test can be JUnit 5
jvm.inspections.junit5.converter.problem.descriptor=#ref can be JUnit 5 test
jvm.inspections.junit5.converter.quickfix=Migrate to JUnit 5
@@ -0,0 +1,45 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection.test.junit
import com.intellij.analysis.JvmAnalysisBundle
import com.intellij.codeInspection.AbstractBaseUastLocalInspectionTool
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.codeInspection.registerUProblem
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.InheritanceUtil
import com.intellij.psi.util.PsiUtil
import com.intellij.uast.UastHintedVisitorAdapter
import com.siyeh.ig.junit.JUnitCommonClassNames.JUNIT_FRAMEWORK_TEST_CASE
import com.siyeh.ig.junit.JUnitCommonClassNames.ORG_JUNIT_TEST
import org.jetbrains.uast.UClass
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
class JUnit4ConverterInspection : AbstractBaseUastLocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
if (!PsiUtil.isLanguageLevel5OrHigher(holder.file)) return PsiElementVisitor.EMPTY_VISITOR
if (JavaPsiFacade.getInstance(holder.project).findClass(JUNIT_FRAMEWORK_TEST_CASE, holder.file.resolveScope) == null) { // junit 3
return PsiElementVisitor.EMPTY_VISITOR
}
if (JavaPsiFacade.getInstance(holder.project).findClass(ORG_JUNIT_TEST, holder.file.resolveScope) == null) { // junit 4
return PsiElementVisitor.EMPTY_VISITOR
}
return UastHintedVisitorAdapter.create(
holder.file.language,
JUnit4ConverterInspectionVisitor(holder),
arrayOf(UClass::class.java),
directOnly = true
)
}
}
private class JUnit4ConverterInspectionVisitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() {
override fun visitClass(node: UClass): Boolean {
val javaClass = node.javaPsi
if (!InheritanceUtil.isInheritor(javaClass, JUNIT_FRAMEWORK_TEST_CASE)) return true
val message = JvmAnalysisBundle.message("jvm.inspections.junit4.converter.problem.descriptor")
holder.registerUProblem(node, message, JUnit4ConverterQuickfix())
return true
}
}
@@ -0,0 +1,311 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection.test.junit
import com.intellij.analysis.JvmAnalysisBundle
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.isInheritorOf
import com.intellij.lang.jvm.JvmModifier
import com.intellij.lang.jvm.actions.*
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.project.Project
import com.intellij.psi.CommonClassNames.JAVA_LANG_CLASS
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.SmartPointerManager
import com.intellij.refactoring.BaseRefactoringProcessor.ConflictsInTestsException
import com.intellij.refactoring.ui.ConflictsDialog
import com.intellij.refactoring.util.CommonRefactoringUtil
import com.intellij.refactoring.util.RefactoringUIUtil
import com.intellij.util.asSafely
import com.intellij.util.containers.MultiMap
import com.siyeh.ig.callMatcher.CallMatcher
import com.siyeh.ig.junit.JUnitCommonClassNames.*
import com.siyeh.ig.psiutils.TypeUtils
import org.jetbrains.uast.*
import org.jetbrains.uast.generate.getUastElementFactory
import org.jetbrains.uast.generate.replace
import org.jetbrains.uast.util.isInFinallyBlock
import org.jetbrains.uast.visitor.AbstractUastVisitor
class JUnit4ConverterQuickfix : LocalQuickFix {
override fun getFamilyName(): String = JvmAnalysisBundle.message("jvm.inspections.junit4.converter.quickfix.name")
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val junit3Class = descriptor.psiElement.getUastParentOfType<UClass>() ?: return
val conflicts = findConflicts(junit3Class)
val runnable = Runnable { WriteAction.run<RuntimeException> { performConversion(junit3Class) } }
if (!conflicts.isEmpty) {
if (ApplicationManager.getApplication().isUnitTestMode && !ConflictsInTestsException.isTestIgnore()) {
throw ConflictsInTestsException(conflicts.values())
}
else if (!ConflictsDialog(junit3Class.javaPsi.project, conflicts, runnable).showAndGet()) return
}
runnable.run()
}
private fun performConversion(junit3Class: UClass) {
for (method in junit3Class.methods) {
val methodPtr = SmartPointerManager.createPointer(method.sourcePsi ?: continue)
if (method.name.startsWith("test")) {
addAnnotation(method, ORG_JUNIT_TEST)
}
else if (method.name == SETUP) {
addAnnotation(method, ORG_JUNIT_BEFORE)
transformSetUpOrTearDownMethod(method)
}
else if (method.name == TEARDOWN) {
addAnnotation(method, ORG_JUNIT_AFTER)
transformSetUpOrTearDownMethod(method)
} else if (method.name == SUITE) {
transformTesSuite(method)
}
methodPtr.element?.toUElementOfType<UMethod>()?.accept(AssertionsConverter())
}
junit3Class.javaPsi.extendsList?.referenceElements?.forEach { it.delete() }
}
private fun findConflicts(junit3Class: UClass): MultiMap<PsiElement, String> {
val conflicts = MultiMap<PsiElement, String>()
findNonMigratableTestSuiteConflicts(junit3Class)?.let { conflicts.putValue(it.first, it.second) }
conflicts.putAllValues(findInheritedUsagesConflicts(junit3Class))
conflicts.putAllValues(findSetupTeardownNameConflicts(junit3Class))
return conflicts
}
private fun findInheritedUsagesConflicts(junit3Class: UClass): MultiMap<PsiElement, String> {
val conflicts = MultiMap<PsiElement, String>()
junit3Class.accept(object : AbstractUastVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
if (migratableMethodNames.contains(node.methodName)) return false
if (node.kind == UastCallKind.CONSTRUCTOR_CALL) return false
val method = node.resolveToUElement()?.asSafely<UMethod>() ?: return false
if (migratableConstructorNames.contains(method.name)) return false
if (method.isStatic) return false // filter out assertions, they will be converted later
val containingMethodClass = method.javaPsi.containingClass ?: return false
if (isAvailableAfterMigration(containingMethodClass.qualifiedName ?: return false)) return false
val sourcePsi = node.sourcePsi ?: return false
val expressionText = CommonRefactoringUtil.htmlEmphasize(sourcePsi.text)
val classText = RefactoringUIUtil.getDescription(junit3Class.javaPsi, false)
val availableSupers = method.javaPsi.findSuperMethods()
.mapNotNull { it.containingClass?.qualifiedName }
.filter { isAvailableAfterMigration(it) }
val problem = if (availableSupers.isNotEmpty()) {
JvmAnalysisBundle.message("jvm.inspections.junit4.converter.quickfix.conflict.semantics", expressionText, classText)
} else {
JvmAnalysisBundle.message("jvm.inspections.junit4.converter.quickfix.conflict.call.compile", expressionText, classText)
}
conflicts.putValue(node.sourcePsi, problem)
return false
}
})
return conflicts
}
private fun isAvailableAfterMigration(fqn: String): Boolean = !fqn.startsWith("junit.framework")
private fun findNonMigratableTestSuiteConflicts(junit3Class: UClass): Pair<PsiElement, String>? {
var conflict: Pair<PsiElement, String>? = null
junit3Class.accept(object : AbstractUastVisitor() {
override fun visitElement(node: UElement): Boolean = conflict != null
override fun visitMethod(node: UMethod): Boolean {
if (conflict != null) return true
if (node.name == SUITE && !isMigratableTestSuite(node)) {
val containingClass = node.getContainingUClass() ?: return false
val classText = RefactoringUIUtil.getDescription(containingClass.javaPsi, false)
conflict = node to JvmAnalysisBundle.message("jvm.inspections.junit4.converter.quickfix.conflict.suite", classText)
}
return true
}
private fun isMigratableTestSuite(method: UMethod): Boolean {
var migratable = true
method.uastBody?.accept(object : AbstractUastVisitor() {
override fun visitElement(node: UElement): Boolean = !migratable
override fun visitCallExpression(node: UCallExpression): Boolean {
if (!migratable) return true
when (node.kind) {
UastCallKind.METHOD_CALL -> {
when {
addTestMatcher.uCallMatches(node) -> {
val argumentExpr = node.valueArguments.first().getUCallExpression(searchLimit = 2) ?: return false
if (argumentExpr.isSuite()) return true
migratable = false
}
addTestSuiteMatcher.uCallMatches(node) -> { }
else -> migratable = false
}
}
UastCallKind.CONSTRUCTOR_CALL -> {
if (node.returnType?.isInheritorOf(JUNIT_FRAMEWORK_TEST_SUITE) == false) migratable = false
}
else -> migratable = false
}
return true
}
override fun visitReturnExpression(node: UReturnExpression): Boolean {
if (!migratable) return true
if (node.returnExpression?.getExpressionType()?.isInheritorOf(JUNIT_FRAMEWORK_TEST_SUITE) == false) migratable = false
return false
}
})
return migratable
}
})
return conflict
}
fun UCallExpression.isSuite() = valueArgumentCount == 0
&& methodName == SUITE
&& TypeUtils.resolvedClassName(returnType) == JUNIT_FRAMEWORK_TEST
private fun findSetupTeardownNameConflicts(junit3Class: UClass): MultiMap<PsiElement, String> {
val conflicts = MultiMap<PsiElement, String>()
junit3Class.accept(object : AbstractUastVisitor() {
override fun visitMethod(node: UMethod): Boolean {
if (node.name != TEARDOWN && node.name != SETUP) return true
val superMethods = node.javaPsi.findSuperMethods().toMutableList()
for (method in superMethods) {
if (method.containingClass?.qualifiedName == JUNIT_FRAMEWORK_TEST_CASE) {
superMethods.remove(method)
break
}
}
if (superMethods.isEmpty()) return true
conflicts.putValue(node, JvmAnalysisBundle.message(
"jvm.inspections.junit4.converter.quickfix.conflict.name", node.uastAnchor?.sourcePsi?.text
))
return true
}
})
return conflicts
}
private fun addAnnotation(aClass: PsiClass, fqn: String, vararg parameters: AnnotationAttributeRequest) {
createAddAnnotationActions(aClass, annotationRequest(fqn, *parameters)).forEach {
it.invoke(aClass.project, null, aClass.containingFile)
}
}
private fun addAnnotation(method: UMethod, fqn: String, vararg parameters: AnnotationAttributeRequest) {
createAddAnnotationActions(method.javaPsi, annotationRequest(fqn, *parameters)).forEach {
it.invoke(method.javaPsi.project, null, method.sourcePsi?.containingFile)
}
}
private fun transformSetUpOrTearDownMethod(method: UMethod) {
val fixModifierOverrideActions = createModifierActions(method, modifierRequest(JvmModifier.PUBLIC, true)) +
createChangeOverrideActions(method, shouldBePresent = false)
fixModifierOverrideActions.forEach { it.invoke(method.javaPsi.project, null, method.javaPsi.containingFile) }
method.accept(SuperCallRemoverVisitor(method.name))
}
private fun transformTesSuite(method: UMethod) {
val containingClass = method.javaPsi.containingClass ?: return
val classValues = findAddedTestSuites(method).map { AnnotationAttributeValueRequest.ClassValue(it) }
addAnnotation(containingClass, ORG_JUNIT_RUNNERS_SUITE_SUITE_CLASSES, arrayAttribute("value", classValues))
addAnnotation(containingClass, ORG_JUNIT_RUNNER_RUN_WITH, classAttribute("value", ORG_JUNIT_RUNNERS_SUITE))
method.sourcePsi?.delete()
}
private fun findAddedTestSuites(method: UMethod): List<String> {
val classLiterals = mutableListOf<String>()
method.uastBody?.accept(object : AbstractUastVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
when {
addTestMatcher.uCallMatches(node) -> {
val qualified = node.valueArguments.first().asSafely<UQualifiedReferenceExpression>()
if(qualified?.selector?.asSafely<UCallExpression>()?.isSuite() == false) return false // don't show conflict in preview
val receiver = qualified?.receiver.asSafely<UReferenceExpression>() ?: return false
classLiterals.add(receiver.getQualifiedName() ?: return false)
}
addTestSuiteMatcher.uCallMatches(node) -> {
val type = node.valueArguments
.first().getQualifiedChain()
.first().asSafely<UClassLiteralExpression>()
?.type ?: return false
classLiterals.add(TypeUtils.resolvedClassName(type) ?: return false)
}
}
return false
}
})
return classLiterals
}
private class SuperCallRemoverVisitor(private val methodName: String) : AbstractUastVisitor() {
override fun visitSuperExpression(node: USuperExpression): Boolean {
val qualifiedSuper = node.uastParent.asSafely<UQualifiedReferenceExpression>() ?: return false
val selector = qualifiedSuper.selector.asSafely<UCallExpression>() ?: return false
if (selector.methodName != methodName) return false
if (qualifiedSuper.isInFinallyBlock()) { // tearDown methods are often in finally clauses
val tryExpression = qualifiedSuper.getParentOfType(
parentClass = UTryExpression::class.java,
strict = true,
terminators = arrayOf(UMethod::class.java)
) ?: return false
val finallyClause = tryExpression.finallyClause ?: return false
if (finallyClause.asSafely<UBlockExpression>()?.expressions?.size == 1) {
val catchClauses = tryExpression.catchClauses
if (catchClauses.isEmpty() && !tryExpression.hasResources) {
val tryExprSrcPsi = tryExpression.sourcePsi ?: return false
val tryClauseSrcPsi = tryExpression.tryClause.sourcePsi ?: return false
val first = tryClauseSrcPsi.firstChild?.nextSibling
val last = tryClauseSrcPsi.lastChild?.prevSibling
tryExprSrcPsi.parent?.addRangeAfter(first, last, tryExpression.sourcePsi)
tryExprSrcPsi.delete()
} else {
finallyClause.sourcePsi?.delete()
}
return false
}
}
qualifiedSuper.sourcePsi?.delete()
return false
}
}
private class AssertionsConverter : AbstractUastVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
if (!node.isPsiValid) return true
val method = node.resolveToUElement()?.asSafely<UMethod>() ?: return false
if (!method.isStatic) return false // assert methods are always static
val containingMethodClass = method.javaPsi.containingClass ?: return false
val containingClassFqn = containingMethodClass.qualifiedName
if (containingClassFqn != JUNIT_FRAMEWORK_ASSERT && containingClassFqn != JUNIT_FRAMEWORK_TEST_CASE) return false
val project = node.sourcePsi?.project ?: return true
val elementFactory = node.getUastElementFactory(project) ?: return false
val newCall = elementFactory.createCallExpression(
elementFactory.createQualifiedReference(ORG_JUNIT_ASSERT, node.sourcePsi),
method.name, node.valueArguments, node.returnType, UastCallKind.METHOD_CALL
) ?: return false
node.replace(newCall)
return false
}
}
companion object {
private const val SETUP = "setUp"
private const val TEARDOWN = "tearDown"
private const val SUITE = "suite"
private const val ADD_TEST = "addTest"
private const val ADD_TEST_SUITE = "addTestSuite"
private val migratableMethodNames = listOf(SETUP, TEARDOWN, SUITE, ADD_TEST, ADD_TEST_SUITE)
private val migratableConstructorNames = listOf("TestSuite")
private val addTestMatcher = CallMatcher.instanceCall(JUNIT_FRAMEWORK_TEST_SUITE, ADD_TEST)
.parameterTypes(JUNIT_FRAMEWORK_TEST)
private val addTestSuiteMatcher = CallMatcher.instanceCall(JUNIT_FRAMEWORK_TEST_SUITE, ADD_TEST_SUITE)
.parameterTypes(JAVA_LANG_CLASS)
}
}
@@ -0,0 +1,289 @@
package com.intellij.codeInspection.tests.java.test.junit
import com.intellij.codeInspection.tests.ULanguage
import com.intellij.codeInspection.tests.test.junit.JUnit4ConverterInspectionTestBase
import com.intellij.refactoring.BaseRefactoringProcessor
class JavaJUnit4ConverterInspectionTest : JUnit4ConverterInspectionTestBase() {
fun `test highlighting`() {
myFixture.testHighlighting(ULanguage.JAVA, """
import junit.framework.TestCase;
class <warning descr="'JUnit3Test' could be converted to JUnit4 test case">JUnit3Test</warning> extends TestCase {
public void testAddition() {
assertEquals(2, 1 + 1);
}
}""".trimIndent())
}
fun `test quickfix lifecycle method name conflict`() {
myFixture.addFileToProject("AbstractJUnit3Test.java", """
import junit.framework.TestCase;
public abstract class AbstractJUnit3Test extends TestCase {
@Override
public void setUp() {
System.out.println("setup 2");
super.setUp();
}
@Override
void tearDown() {
try {
System.out.println("tearDown 2");
} finally {
super.tearDown();
}
}
}
""".trimIndent())
myFixture.configureByText("JUnit3Test.java", """
import junit.framework.TestCase;
class JUnit3<caret>Test extends AbstractJUnit3Test {
@Override
public void setUp() {
System.out.println("setup 1");
super.setUp();
}
public void testAddition() {
assertEquals(2, 1 + 1);
}
@Override
void tearDown() {
try {
System.out.println("tearDown 1");
} finally {
super.tearDown();
}
}
}
""".trimIndent())
try {
myFixture.runQuickFix("Convert to JUnit 4 test case")
fail("Expected ConflictsInTestsException exception te be thrown.")
} catch(e: BaseRefactoringProcessor.ConflictsInTestsException) {
assertEquals(e.messages.size, 2)
assertContainsElements(
e.messages,
"Method setUp will have a name collision with its super method",
"Method tearDown will have a name collision with its super method"
)
}
}
fun `test quickfix semantic change`() {
myFixture.testQuickFixException<BaseRefactoringProcessor.ConflictsInTestsException>(ULanguage.JAVA, """
import junit.framework.TestCase;
class JUnit3<caret>Test extends TestCase {
public void testAddition() {
System.out.println(toString());
}
}
""".trimIndent(), "Convert to JUnit 4 test case") { e ->
assertEquals(e.messages.size, 1)
assertContainsElements(
e.messages,
"Method call toString() may change semantics when class JUnit3Test is converted to JUnit 4"
)
}
}
fun `test quickfix removed method`() {
myFixture.testQuickFixException<BaseRefactoringProcessor.ConflictsInTestsException>(ULanguage.JAVA, """
import junit.framework.TestCase;
class JUnit3<caret>Test extends TestCase {
public void testAddition() {
System.out.println(countTestCases());
}
}
""".trimIndent(), "Convert to JUnit 4 test case") { e ->
assertEquals(e.messages.size, 1)
assertContainsElements(
e.messages,
"Method call countTestCases() will not compile when class JUnit3Test is converted to JUnit 4"
)
}
}
fun `test quickfix non convertable suite`() {
myFixture.testQuickFixException<BaseRefactoringProcessor.ConflictsInTestsException>(ULanguage.JAVA, """
import junit.framework.TestCase;
import junit.framework.Test;
class JUnit3<caret>Test extends TestCase {
public static Test suite() {
System.out.println("Creating test suite");
TestSuite suite = new TestSuite();
suite.addTestSuite(Foo.class);
suite.addTestSuite(Bar.class);
return suite;
}
}
""".trimIndent(), "Convert to JUnit 4 test case") { e ->
assertEquals(e.messages.size, 1)
assertContainsElements(
e.messages,
"Migration of suite method for class JUnit3Test has side effects which will be deleted"
)
}
}
fun `test quickfix class expression suite converter`() {
myFixture.testQuickFix(ULanguage.JAVA, """
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.framework.Test;
class Foo extends TestCase { }
class Bar extends TestCase { }
class JUnit3<caret>Test extends TestCase {
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTestSuite(Foo.class);
suite.addTestSuite(Bar.class);
return suite;
}
}
""".trimIndent(), """
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.framework.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
class Foo extends TestCase { }
class Bar extends TestCase { }
@RunWith(Suite.class)
@Suite.SuiteClasses({Foo.class, Bar.class})
class JUnit3Test {
}
""".trimIndent(), "Convert to JUnit 4 test case")
}
fun `test quickfix nested suite converter`() {
myFixture.testQuickFix(ULanguage.JAVA, """
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.framework.Test;
class Foo extends TestCase {
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTestSuite(Bar.class);
return suite;
}
}
class Bar extends TestCase { }
class JUnit3<caret>Test extends TestCase {
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(Foo.suite());
return suite;
}
}
""".trimIndent(), """
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.framework.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
class Foo extends TestCase {
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTestSuite(Bar.class);
return suite;
}
}
class Bar extends TestCase { }
@RunWith(Suite.class)
@Suite.SuiteClasses({Foo.class})
class JUnit3Test {
}
""".trimIndent(), "Convert to JUnit 4 test case")
}
fun `test quickfix assertion converter`() {
myFixture.testQuickFix(ULanguage.JAVA, """
import junit.framework.TestCase;
class JUnit3<caret>Test extends TestCase {
public void testAddition() {
assertEquals(2, 1 + 1);
}
}
""".trimIndent(), """
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Test;
class JUnit3Test {
@Test
public void testAddition() {
Assert.assertEquals(2, 1 + 1);
}
}
""".trimIndent(), "Convert to JUnit 4 test case")
}
fun `test quickfix setup and teardown converter`() {
myFixture.testQuickFix(ULanguage.JAVA, """
import junit.framework.TestCase;
class JUnit3<caret>Test extends TestCase {
@Override
public void setUp() {
System.out.println("setup");
super.setUp();
}
public void testAddition() {
assertEquals(2, 1 + 1);
}
@Override
void tearDown() {
try {
System.out.println("tearDown");
} finally {
super.tearDown();
}
}
}
""".trimIndent(), """
import junit.framework.TestCase;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
class JUnit3Test {
@Before
public void setUp() {
System.out.println("setup");
}
@Test
public void testAddition() {
Assert.assertEquals(2, 1 + 1);
}
@After
public void tearDown() {
System.out.println("tearDown");
}
}
""".trimIndent(), "Convert to JUnit 4 test case")
}
}
@@ -0,0 +1,319 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInspection.tests.kotlin.test.junit
import com.intellij.codeInspection.tests.ULanguage
import com.intellij.codeInspection.tests.test.junit.JUnit4ConverterInspectionTestBase
import com.intellij.openapi.module.Module
import com.intellij.openapi.roots.ContentEntry
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.refactoring.BaseRefactoringProcessor
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.PsiTestUtil
import com.intellij.util.PathUtil
import java.io.File
class KotlinJUnit4ConverterInspectionTest : JUnit4ConverterInspectionTestBase() {
override fun getProjectDescriptor(): LightProjectDescriptor = object : JUnitProjectDescriptor(sdkLevel) {
override fun configureModule(module: Module, model: ModifiableRootModel, contentEntry: ContentEntry) {
super.configureModule(module, model, contentEntry)
val jar = File(PathUtil.getJarPathForClass(JvmStatic::class.java))
PsiTestUtil.addLibrary(model, "kotlin-stdlib", jar.parent, jar.name)
}
}
fun `test highlighting`() {
myFixture.testHighlighting(ULanguage.KOTLIN, """
import junit.framework.TestCase
class <warning descr="'JUnit3Test' could be converted to JUnit4 test case">JUnit3Test</warning> : TestCase() {
fun testAddition() {
assertEquals(2, 1 + 1)
}
}""".trimIndent())
}
fun `test quickfix lifecycle method name conflict`() {
myFixture.addFileToProject("AbstractJUnit3Test.kt", """
import junit.framework.TestCase
public abstract class AbstractJUnit3Test : TestCase() {
override fun setUp() {
println("setup 2")
super.setUp()
}
override fun tearDown() {
try {
println("tearDown 2")
} finally {
super.tearDown()
}
}
}
""".trimIndent())
myFixture.configureByText("JUnit3Test.kt", """
import junit.framework.TestCase
class JUnit3<caret>Test : AbstractJUnit3Test() {
override fun setUp() {
println("setup 1")
super.setUp()
}
fun testAddition() {
assertEquals(2, 1 + 1)
}
override fun tearDown() {
try {
println("tearDown 1")
} finally {
super.tearDown()
}
}
}
""".trimIndent())
try {
myFixture.runQuickFix("Convert to JUnit 4 test case")
fail("Expected ConflictsInTestsException exception te be thrown.")
} catch(e: BaseRefactoringProcessor.ConflictsInTestsException) {
assertEquals(e.messages.size, 2)
assertContainsElements(
e.messages,
"Method setUp will have a name collision with its super method",
"Method tearDown will have a name collision with its super method"
)
}
}
fun `test quickfix semantic change`() {
myFixture.testQuickFixException<BaseRefactoringProcessor.ConflictsInTestsException>(ULanguage.KOTLIN, """
import junit.framework.TestCase
class JUnit3<caret>Test : TestCase() {
fun testAddition() {
println(toString())
}
}
""".trimIndent(), "Convert to JUnit 4 test case") { e ->
assertEquals(e.messages.size, 1)
assertContainsElements(
e.messages,
"Method call toString() may change semantics when class JUnit3Test is converted to JUnit 4"
)
}
}
fun `test quickfix removed method`() {
myFixture.testQuickFixException<BaseRefactoringProcessor.ConflictsInTestsException>(ULanguage.KOTLIN, """
import junit.framework.TestCase
class JUnit3<caret>Test : TestCase() {
fun testAddition() {
println(countTestCases())
}
}
""".trimIndent(), "Convert to JUnit 4 test case") { e ->
assertEquals(e.messages.size, 1)
assertContainsElements(
e.messages,
"Method call countTestCases() will not compile when class JUnit3Test is converted to JUnit 4"
)
}
}
fun `test quickfix non convertable suite`() {
myFixture.testQuickFixException<BaseRefactoringProcessor.ConflictsInTestsException>(ULanguage.KOTLIN, """
import junit.framework.TestCase
import junit.framework.Test
class JUnit3<caret>Test : TestCase() {
companion object {
@JvmStatic
fun suite(): Test {
println("Creating test suite")
val suite = TestSuite()
suite.addTestSuite(Foo::class.java)
suite.addTestSuite(Bar::class.java)
return suite
}
}
}
""".trimIndent(), "Convert to JUnit 4 test case") { e ->
assertEquals(e.messages.size, 1)
assertContainsElements(
e.messages,
"Migration of suite method for class JUnit3Test has side effects which will be deleted"
)
}
}
fun `test quickfix class expression suite converter`() {
myFixture.testQuickFix(ULanguage.KOTLIN, """
import junit.framework.TestCase
import junit.framework.TestSuite
import junit.framework.Test
class Foo : TestCase() { }
class Bar : TestCase() { }
class JUnit3<caret>Test : TestCase() {
companion object {
@JvmStatic
fun suite(): Test {
val suite = TestSuite()
suite.addTestSuite(Foo::class.java)
suite.addTestSuite(Bar::class.java)
return suite
}
}
}
""".trimIndent(), """
import junit.framework.TestCase
import junit.framework.TestSuite
import junit.framework.Test
import org.junit.runner.RunWith
import org.junit.runners.Suite
class Foo : TestCase() { }
class Bar : TestCase() { }
@RunWith(Suite::class)
@Suite.SuiteClasses(Foo::class, Bar::class)
class JUnit3Test {
companion object {
}
}
""".trimIndent(), "Convert to JUnit 4 test case")
}
fun `test quickfix nested suite converter`() {
myFixture.testQuickFix(ULanguage.KOTLIN, """
import junit.framework.TestCase
import junit.framework.TestSuite
import junit.framework.Test
class Foo : TestCase() {
companion object {
@JvmStatic
fun suite(): Test {
val suite = TestSuite()
suite.addTestSuite(Bar::class.java)
return suite
}
}
}
class Bar : TestCase() { }
class JUnit3<caret>Test : TestCase() {
companion object {
@JvmStatic
fun suite(): Test {
val suite = TestSuite()
suite.addTest(Foo.suite())
return suite
}
}
}
""".trimIndent(), """
import junit.framework.TestCase
import junit.framework.TestSuite
import junit.framework.Test
import org.junit.runner.RunWith
import org.junit.runners.Suite
class Foo : TestCase() {
companion object {
@JvmStatic
fun suite(): Test {
val suite = TestSuite()
suite.addTestSuite(Bar::class.java)
return suite
}
}
}
class Bar : TestCase() { }
@RunWith(Suite::class)
@Suite.SuiteClasses(Foo.Companion::class)
class JUnit3Test {
companion object {
}
}
""".trimIndent(), "Convert to JUnit 4 test case")
}
fun `test quickfix assertion converter`() {
myFixture.testQuickFix(ULanguage.KOTLIN, """
import junit.framework.TestCase
class JUnit3<caret>Test : TestCase {
fun testAddition() {
assertEquals(2, 1 + 1)
}
}
""".trimIndent(), """
import junit.framework.TestCase
import org.junit.Assert
import org.junit.Test
class JUnit3Test {
@Test
fun testAddition() {
Assert.assertEquals(2, 1 + 1)
}
}
""".trimIndent(), "Convert to JUnit 4 test case")
}
fun `test quickfix setup and teardown converter`() {
myFixture.testQuickFix(ULanguage.KOTLIN, """
import junit.framework.TestCase
class JUnit3<caret>Test : TestCase {
override fun setUp() {
println("setup")
super.setUp()
}
fun testAddition() {
assertEquals(2, 1 + 1)
}
override fun tearDown() {
try {
println("tearDown")
} finally {
super.tearDown()
}
}
}
""".trimIndent(), """
import junit.framework.TestCase
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
class JUnit3Test {
@Before
fun setUp() {
println("setup")
}
@Test
fun testAddition() {
Assert.assertEquals(2, 1 + 1)
}
@After
fun tearDown() {
println("tearDown")
}
}
""".trimIndent(), "Convert to JUnit 4 test case")
}
}
@@ -0,0 +1,24 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInspection.tests.test.junit
import com.intellij.codeInspection.test.junit.JUnit4ConverterInspection
import com.intellij.codeInspection.tests.UastInspectionTestBase
import com.intellij.openapi.module.Module
import com.intellij.openapi.roots.ContentEntry
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.pom.java.LanguageLevel
import com.intellij.testFramework.LightProjectDescriptor
abstract class JUnit4ConverterInspectionTestBase : UastInspectionTestBase() {
override val inspection = JUnit4ConverterInspection()
protected open class JUnitProjectDescriptor(languageLevel: LanguageLevel) : ProjectDescriptor(languageLevel) {
override fun configureModule(module: Module, model: ModifiableRootModel, contentEntry: ContentEntry) {
super.configureModule(module, model, contentEntry)
model.addJUnit3Library()
model.addJUnit4Library()
}
}
override fun getProjectDescriptor(): LightProjectDescriptor = JUnitProjectDescriptor(sdkLevel)
}
@@ -1662,9 +1662,6 @@ inspection.simplifiable.compare.java.time.problem.descriptor=Expression with 'ja
redundant.string.format.call.quickfix=Remove redundant call to 'String.format()'
redundant.string.formatted.call.quickfix=Remove redundant call to 'String.formatted()'
junit4.test.method.in.class.extending.junit3.testcase.display.name=JUnit 4 test method in class extending JUnit 3 TestCase
convert.junit3.test.case.error.string=<code>#ref</code> could be converted to JUnit4 test case
convert.junit3.test.case.family.name=Convert to JUnit 4 test case
convert.junit3.test.case.display.name=JUnit 3 test can be JUnit 4
junit4.test.method.in.class.extending.junit3.testcase.problem.descriptor=Method <code>#ref()</code> annotated with '@Test' inside class extending JUnit 3 TestCase #loc
ignore.test.method.in.class.extending.junit3.testcase.problem.descriptor=JUnit 3 test method <code>#ref()</code> annotated with '@Ignore' won't be ignored #loc
ignore.test.method.in.class.extending.junit3.testcase.quickfix=Remove ''@Ignore'' and rename method to ''{0}''
@@ -13,6 +13,7 @@ public final class JUnitCommonClassNames {
public static final String JUNIT_FRAMEWORK_ASSERT = "junit.framework.Assert";
public static final String ORG_JUNIT_JUPITER_API_ASSERTIONS = "org.junit.jupiter.api.Assertions";
public static final String ORG_JUNIT_JUPITER_API_ASSUMPTIONS = "org.junit.jupiter.api.Assumptions";
public static final String JUNIT_FRAMEWORK_TEST = "junit.framework.Test";
public static final String JUNIT_FRAMEWORK_TEST_CASE = "junit.framework.TestCase";
public static final String JUNIT_FRAMEWORK_TEST_SUITE = "junit.framework.TestSuite";
public static final String ORG_JUNIT_TEST = "org.junit.Test";
@@ -25,6 +26,9 @@ public final class JUnitCommonClassNames {
public static final String ORG_JUNIT_AFTER = "org.junit.After";
public static final String ORG_JUNIT_BEFORE_CLASS = "org.junit.BeforeClass";
public static final String ORG_JUNIT_AFTER_CLASS = "org.junit.AfterClass";
public static final String ORG_JUNIT_RUNNER_RUN_WITH = "org.junit.runner.RunWith";
public static final String ORG_JUNIT_RUNNERS_SUITE = "org.junit.runners.Suite";
public static final String ORG_JUNIT_RUNNERS_SUITE_SUITE_CLASSES = "org.junit.runners.Suite.SuiteClasses";
public static final String ORG_JUNIT_JUPITER_PARAMS_PARAMETERIZED_TEST = "org.junit.jupiter.params.ParameterizedTest";
public static final String ORG_JUNIT_JUPITER_PARAMS_PROVIDER_METHOD_SOURCE = "org.junit.jupiter.params.provider.MethodSource";
public static final String ORG_JUNIT_JUPITER_CONDITION_PROVIDER_ENABLED_IF = "org.junit.jupiter.api.condition.EnabledIf";
@@ -1371,10 +1371,6 @@
key="junit4.test.method.in.class.extending.junit3.testcase.display.name" groupBundle="messages.InspectionsBundle"
groupKey="group.names.junit.issues" enabledByDefault="true" level="WARNING"
implementationClass="com.siyeh.ig.junit.JUnit4AnnotatedMethodInJUnit3TestCaseInspection"/>
<localInspection groupPath="Java" language="JAVA" shortName="Junit4Converter" bundle="messages.InspectionGadgetsBundle"
key="convert.junit3.test.case.display.name" groupBundle="messages.InspectionsBundle"
groupKey="group.names.junit.issues" enabledByDefault="true" level="INFORMATION"
implementationClass="com.siyeh.ig.junit.Junit4ConverterInspection"/>
<localInspection groupPath="Java" language="JAVA" shortName="MisorderedAssertEqualsArguments"
bundle="messages.InspectionGadgetsBundle" key="misordered.assert.equals.arguments.display.name"
groupBundle="messages.InspectionsBundle" groupKey="group.names.test.frameworks.issues" enabledByDefault="false" level="WARNING"
@@ -1,80 +0,0 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.siyeh.ig.junit;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.InspectionGadgetsFix;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class Junit4ConverterInspection extends BaseInspection {
@Override
public boolean shouldInspect(@NotNull PsiFile file) {
if (PsiUtil.isLanguageLevel5OrHigher(file)) return true;
return super.shouldInspect(file);
}
@Override
protected @NotNull String buildErrorString(Object... infos) {
return InspectionGadgetsBundle.message("convert.junit3.test.case.error.string");
}
@Override
public BaseInspectionVisitor buildVisitor() {
return new BaseInspectionVisitor() {
@Override
public void visitClass(@NotNull PsiClass aClass) {
super.visitClass(aClass);
if (possibleToConvert(aClass)) {
registerClassError(aClass);
}
}
private boolean possibleToConvert(PsiClass aClass) {
final PsiReferenceList extendsList = aClass.getExtendsList();
if (extendsList == null) return false;
final PsiJavaCodeReferenceElement[] referenceElements = extendsList.getReferenceElements();
if (referenceElements.length != 1) return false;
final PsiJavaCodeReferenceElement referenceElement = referenceElements[0];
final PsiElement target = referenceElement.resolve();
if (!(target instanceof PsiClass)) return false;
final PsiClass targetClass = (PsiClass)target;
final String name = targetClass.getQualifiedName();
if (!"junit.framework.TestCase".equals(name)) return false;
final Project project = aClass.getProject();
final GlobalSearchScope scope = aClass.getResolveScope();
final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
final PsiClass testAnnotation = psiFacade.findClass("org.junit.Test", scope);
return testAnnotation != null;
}
};
}
@Override
protected @Nullable InspectionGadgetsFix buildFix(Object... infos) {
return new InspectionGadgetsFix() {
@Override
protected void doFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiClass pClass = PsiTreeUtil.getParentOfType(descriptor.getPsiElement(), PsiClass.class);
JUnit4AnnotatedMethodInJUnit3TestCaseInspection.convertJUnit3ClassToJUnit4(pClass);
}
@Override
public @NotNull String getFamilyName() {
return InspectionGadgetsBundle.message("convert.junit3.test.case.family.name");
}
};
}
}
@@ -1,61 +0,0 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.siyeh.ig.junit;
import com.intellij.codeInspection.InspectionProfileEntry;
import com.siyeh.ig.LightJavaInspectionTestCase;
import org.jetbrains.annotations.Nullable;
public class Junit4ConverterInspectionTest extends LightJavaInspectionTestCase {
@Override
protected String[] getEnvironmentClasses() {
return new String[]{
"package junit.framework;" +
"public abstract class TestCase extends Assert {\n" +
"}",
"""
package junit.framework;public class Assert { static public void assertEquals(int expected, int actual) {
}
}""",
"package org.junit;" +
"public class Assert {" +
"static public void assertEquals(long expected, long actual) {" +
"}\n" +
"}",
"""
package org.junit;@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test {}"""
};
}
@SuppressWarnings("JUnitTestCaseWithNoTests")
public void testSimple() {
doTest("""
import junit.framework.TestCase;
class /*'JUnit3Test' could be converted to JUnit4 test case*//*_*/JUnit3Test/**/ extends TestCase {
public void testAddition() {
assertEquals(2, 1 + 1);
}
}""");
checkQuickFix("Convert to JUnit 4 test case", """
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Test;
class JUnit3Test {
@Test
public void testAddition() {
Assert.assertEquals(2, 1 + 1);
}
}""");
}
@Override
protected @Nullable InspectionProfileEntry getInspection() {
return new Junit4ConverterInspection();
}
}