From c18db65d6448e4de12d4fb8632c475178fefeda2 Mon Sep 17 00:00:00 2001 From: Bart van Helvert Date: Mon, 7 Nov 2022 13:45:33 +0100 Subject: [PATCH] [jvm] IDEA-283602 Migrate JUnit 4 converter to UAST GitOrigin-RevId: f7bfbf867ecb02a04854bcc266146720ac6a8a6a --- .../resources/META-INF/JvmAnalysisPlugin.xml | 5 + .../Junit4Converter.html | 0 .../messages/JvmAnalysisBundle.properties | 8 + .../test/junit/JUnit4ConverterInspection.kt | 45 +++ .../test/junit/JUnit4ConverterQuickfix.kt | 311 +++++++++++++++++ .../JavaJUnit4ConverterInspectionTest.kt | 289 ++++++++++++++++ .../KotlinJUnit4ConverterInspectionTest.kt | 319 ++++++++++++++++++ .../JUnit4ConverterInspectionTestBase.kt | 24 ++ .../InspectionGadgetsBundle.properties | 3 - .../siyeh/ig/junit/JUnitCommonClassNames.java | 4 + .../src/META-INF/InspectionGadgets.xml | 4 - .../ig/junit/Junit4ConverterInspection.java | 80 ----- .../junit/Junit4ConverterInspectionTest.java | 61 ---- 13 files changed, 1005 insertions(+), 148 deletions(-) rename {plugins/InspectionGadgets/src => jvm/jvm-analysis-impl/resources}/inspectionDescriptions/Junit4Converter.html (100%) create mode 100644 jvm/jvm-analysis-impl/src/com/intellij/codeInspection/test/junit/JUnit4ConverterInspection.kt create mode 100644 jvm/jvm-analysis-impl/src/com/intellij/codeInspection/test/junit/JUnit4ConverterQuickfix.kt create mode 100644 jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/test/junit/JavaJUnit4ConverterInspectionTest.kt create mode 100644 jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/codeInspection/tests/kotlin/test/junit/KotlinJUnit4ConverterInspectionTest.kt create mode 100644 jvm/jvm-analysis-tests/src/com/intellij/codeInspection/tests/test/junit/JUnit4ConverterInspectionTestBase.kt delete mode 100644 plugins/InspectionGadgets/src/com/siyeh/ig/junit/Junit4ConverterInspection.java delete mode 100644 plugins/InspectionGadgets/testsrc/com/siyeh/ig/junit/Junit4ConverterInspectionTest.java diff --git a/jvm/jvm-analysis-impl/resources/META-INF/JvmAnalysisPlugin.xml b/jvm/jvm-analysis-impl/resources/META-INF/JvmAnalysisPlugin.xml index 6c3fcc4b374b..4bd4de85d52c 100644 --- a/jvm/jvm-analysis-impl/resources/META-INF/JvmAnalysisPlugin.xml +++ b/jvm/jvm-analysis-impl/resources/META-INF/JvmAnalysisPlugin.xml @@ -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"/> + #ref() 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=#ref 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 diff --git a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/test/junit/JUnit4ConverterInspection.kt b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/test/junit/JUnit4ConverterInspection.kt new file mode 100644 index 000000000000..50b77a757a58 --- /dev/null +++ b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/test/junit/JUnit4ConverterInspection.kt @@ -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 + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/test/junit/JUnit4ConverterQuickfix.kt b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/test/junit/JUnit4ConverterQuickfix.kt new file mode 100644 index 000000000000..f798674a25a8 --- /dev/null +++ b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/test/junit/JUnit4ConverterQuickfix.kt @@ -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() ?: return + val conflicts = findConflicts(junit3Class) + + val runnable = Runnable { WriteAction.run { 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()?.accept(AssertionsConverter()) + } + junit3Class.javaPsi.extendsList?.referenceElements?.forEach { it.delete() } + } + + private fun findConflicts(junit3Class: UClass): MultiMap { + val conflicts = MultiMap() + 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 { + val conflicts = MultiMap() + 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() ?: 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? { + var conflict: Pair? = 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 { + val conflicts = MultiMap() + 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 { + val classLiterals = mutableListOf() + method.uastBody?.accept(object : AbstractUastVisitor() { + override fun visitCallExpression(node: UCallExpression): Boolean { + when { + addTestMatcher.uCallMatches(node) -> { + val qualified = node.valueArguments.first().asSafely() + if(qualified?.selector?.asSafely()?.isSuite() == false) return false // don't show conflict in preview + val receiver = qualified?.receiver.asSafely() ?: return false + classLiterals.add(receiver.getQualifiedName() ?: return false) + } + addTestSuiteMatcher.uCallMatches(node) -> { + val type = node.valueArguments + .first().getQualifiedChain() + .first().asSafely() + ?.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() ?: return false + val selector = qualifiedSuper.selector.asSafely() ?: 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()?.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() ?: 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) + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/test/junit/JavaJUnit4ConverterInspectionTest.kt b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/test/junit/JavaJUnit4ConverterInspectionTest.kt new file mode 100644 index 000000000000..4c0289df5916 --- /dev/null +++ b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/test/junit/JavaJUnit4ConverterInspectionTest.kt @@ -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 JUnit3Test 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 JUnit3Test 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(ULanguage.JAVA, """ + import junit.framework.TestCase; + + class JUnit3Test 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(ULanguage.JAVA, """ + import junit.framework.TestCase; + + class JUnit3Test 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(ULanguage.JAVA, """ + import junit.framework.TestCase; + import junit.framework.Test; + + class JUnit3Test 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 JUnit3Test 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 JUnit3Test 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 JUnit3Test 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 JUnit3Test 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") + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/codeInspection/tests/kotlin/test/junit/KotlinJUnit4ConverterInspectionTest.kt b/jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/codeInspection/tests/kotlin/test/junit/KotlinJUnit4ConverterInspectionTest.kt new file mode 100644 index 000000000000..e71314225656 --- /dev/null +++ b/jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/codeInspection/tests/kotlin/test/junit/KotlinJUnit4ConverterInspectionTest.kt @@ -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 JUnit3Test : 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 JUnit3Test : 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(ULanguage.KOTLIN, """ + import junit.framework.TestCase + + class JUnit3Test : 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(ULanguage.KOTLIN, """ + import junit.framework.TestCase + + class JUnit3Test : 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(ULanguage.KOTLIN, """ + import junit.framework.TestCase + import junit.framework.Test + + class JUnit3Test : 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 JUnit3Test : 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 JUnit3Test : 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 JUnit3Test : 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 JUnit3Test : 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") + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-tests/src/com/intellij/codeInspection/tests/test/junit/JUnit4ConverterInspectionTestBase.kt b/jvm/jvm-analysis-tests/src/com/intellij/codeInspection/tests/test/junit/JUnit4ConverterInspectionTestBase.kt new file mode 100644 index 000000000000..49ef98df5b96 --- /dev/null +++ b/jvm/jvm-analysis-tests/src/com/intellij/codeInspection/tests/test/junit/JUnit4ConverterInspectionTestBase.kt @@ -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) +} \ No newline at end of file diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/resources/messages/InspectionGadgetsBundle.properties b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/resources/messages/InspectionGadgetsBundle.properties index 078e5fca1a10..316bce4313f1 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/resources/messages/InspectionGadgetsBundle.properties +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/resources/messages/InspectionGadgetsBundle.properties @@ -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=#ref 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 #ref() annotated with '@Test' inside class extending JUnit 3 TestCase #loc ignore.test.method.in.class.extending.junit3.testcase.problem.descriptor=JUnit 3 test method #ref() annotated with '@Ignore' won't be ignored #loc ignore.test.method.in.class.extending.junit3.testcase.quickfix=Remove ''@Ignore'' and rename method to ''{0}'' diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/junit/JUnitCommonClassNames.java b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/junit/JUnitCommonClassNames.java index 9d106cde27ca..12ae59dd727c 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/junit/JUnitCommonClassNames.java +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/junit/JUnitCommonClassNames.java @@ -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"; diff --git a/plugins/InspectionGadgets/src/META-INF/InspectionGadgets.xml b/plugins/InspectionGadgets/src/META-INF/InspectionGadgets.xml index 5250fb694248..1e08ec8ade61 100644 --- a/plugins/InspectionGadgets/src/META-INF/InspectionGadgets.xml +++ b/plugins/InspectionGadgets/src/META-INF/InspectionGadgets.xml @@ -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"/> -