diff --git a/.idea/libraries/JUnit6.xml b/.idea/libraries/JUnit6.xml new file mode 100644 index 000000000000..dcdbd5ccd639 --- /dev/null +++ b/.idea/libraries/JUnit6.xml @@ -0,0 +1,38 @@ + + + + + + db1404414d23effbeb1202025f1e984f9069ddeed6d4c1826bb70bce8c70fb31 + + + 48e2df636cab6563ced64dcdff8abb2355627cb236ef0bf37598682ddf742f1b + + + 7f7d65576d02547d4d6b44ec3d49dd37904cd8927baae5bddd541cd24d513a39 + + + b509448ac506d607319f182537f0b35d71007582ec741832a1f111e5b5b70b38 + + + 1fad6e6be7557781e4d33729d49ae1cdc8fdda6fe477bb0cc68ce351eafdfbab + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/JUnit6Launcher.xml b/.idea/libraries/JUnit6Launcher.xml new file mode 100644 index 000000000000..5cc8cca3c7fc --- /dev/null +++ b/.idea/libraries/JUnit6Launcher.xml @@ -0,0 +1,33 @@ + + + + + + 3c41ec667a185ec0b51ca8d9d68e5f0e425d6645c6e4eb64b8a81716a4deaed1 + + + 8f5e8cb552c311d4a236334975a5ff9ec4db5f64e84151d01500f31fa19edfba + + + 1fad6e6be7557781e4d33729d49ae1cdc8fdda6fe477bb0cc68ce351eafdfbab + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index e6beeecda0c8..0d81b84c65df 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -590,6 +590,8 @@ + + diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt index 3b7ba9ac12c8..840f354f5303 100644 --- a/build/bazel-generated-file-list.txt +++ b/build/bazel-generated-file-list.txt @@ -885,6 +885,8 @@ plugins/junit/kotlin-tests-shared plugins/junit/testFramework plugins/junit5_rt plugins/junit5_rt_tests +plugins/junit6_rt +plugins/junit6_rt_tests plugins/junit_rt plugins/keymaps/eclipse-keymap plugins/keymaps/netbeans5.6-keymap diff --git a/java/execution/impl/src/com/intellij/execution/junit/InheritorChooser.java b/java/execution/impl/src/com/intellij/execution/junit/InheritorChooser.java index 7ff209d98780..d2d5be41498e 100644 --- a/java/execution/impl/src/com/intellij/execution/junit/InheritorChooser.java +++ b/java/execution/impl/src/com/intellij/execution/junit/InheritorChooser.java @@ -1,4 +1,4 @@ -// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.execution.junit; import com.intellij.execution.ExecutionBundle; @@ -71,7 +71,7 @@ public class InheritorChooser { } if (classes.size() == 1) { - runForClass(classes.get(0), psiMethod, context, performRunnable); + runForClass(classes.getFirst(), psiMethod, context, performRunnable); return true; } if (classes.isEmpty()) return false; @@ -83,7 +83,7 @@ public class InheritorChooser { final List psiClasses = new ArrayList<>(Arrays.asList(((PsiClassOwner)containingFile).getClasses())); psiClasses.retainAll(classes); if (psiClasses.size() == 1) { - runForClass(psiClasses.get(0), psiMethod, context, performRunnable); + runForClass(psiClasses.getFirst(), psiMethod, context, performRunnable); return true; } } @@ -107,7 +107,7 @@ public class InheritorChooser { classes.sort(renderer.getComparator()); //suggest to run all inherited tests - classes.add(0, null); + classes.addFirst(null); String locationName = psiMethod != null ? psiMethod.getName() : containingClass.getName(); JBPopupFactory.getInstance() .createPopupChooserBuilder(classes) @@ -137,7 +137,7 @@ public class InheritorChooser { return false; } - private static void collectClasses(PsiClass containingClass, List classes, HashSet visited) { + private static void collectClasses(PsiClass containingClass, List classes, Set visited) { if (!visited.add(containingClass)) return; Set containers = new HashSet<>(); final boolean isJUnit5 = ReadAction.compute(() -> JUnitUtil.isJUnit5(containingClass)); diff --git a/java/execution/impl/src/com/intellij/execution/junit/JUnitUtil.java b/java/execution/impl/src/com/intellij/execution/junit/JUnitUtil.java index 5a33df8d0a85..d3373bc26713 100644 --- a/java/execution/impl/src/com/intellij/execution/junit/JUnitUtil.java +++ b/java/execution/impl/src/com/intellij/execution/junit/JUnitUtil.java @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.execution.junit; import com.intellij.codeInsight.AnnotationUtil; @@ -38,7 +38,7 @@ public final class JUnitUtil { public static final String RULE_ANNOTATION = "org.junit.Rule"; public static final String TEST5_PACKAGE_FQN = "org.junit.jupiter.api"; public static final String TEST5_ANNOTATION = "org.junit.jupiter.api.Test"; - + public static final String CUSTOM_TESTABLE_ANNOTATION = "org.junit.platform.commons.annotation.Testable"; public static final Set CUSTOM_TESTABLE_ANNOTATION_LIST = Collections.singleton(CUSTOM_TESTABLE_ANNOTATION); @@ -304,7 +304,6 @@ public final class JUnitUtil { return true; } - public static boolean isJUnit5TestClass(final @NotNull PsiClass psiClass, boolean checkAbstract) { final PsiModifierList modifierList = psiClass.getModifierList(); if (modifierList == null) return false; diff --git a/java/java-analysis-impl/src/com/siyeh/ig/junit/JUnitCommonClassNames.java b/java/java-analysis-impl/src/com/siyeh/ig/junit/JUnitCommonClassNames.java index e7c8465dab09..89fac8c1d913 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/junit/JUnitCommonClassNames.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/junit/JUnitCommonClassNames.java @@ -68,6 +68,7 @@ public final @NonNls class JUnitCommonClassNames { public static final String ORG_JUNIT_JUPITER_API_EXTENSION_EXTEND_WITH = "org.junit.jupiter.api.extension.ExtendWith"; public static final String ORG_JUNIT_JUPITER_API_TEST_INSTANCE = "org.junit.jupiter.api.TestInstance"; public static final String ORG_JUNIT_PLATFORM_ENGINE_TEST_ENGINE = "org.junit.platform.engine.TestEngine"; + public static final String ORG_JUNIT_PLATFORM_ENGINE_CANCELLATION_TOKEN = "org.junit.platform.engine.CancellationToken"; public static final String ORG_JUNIT_PLATFORM_ENGINE = "org.junit.platform.engine"; public static final String ORG_JUNIT_JUPITER_API_EXTENSION_REGISTER_EXTENSION = "org.junit.jupiter.api.extension.RegisterExtension"; public static final String ORG_JUNIT_JUPITER_API_EXTENSION_BEFORE_ALL_CALLBACK = "org.junit.jupiter.api.extension.BeforeAllCallback"; @@ -83,4 +84,7 @@ public final @NonNls class JUnitCommonClassNames { public static final String ORG_JUNIT_EXPERIMENTAL_THEORIES_DATAPOINTS = "org.junit.experimental.theories.DataPoints"; public static final String ORG_JUNIT_EXPERIMENTAL_RUNNERS_ENCLOSED = "org.junit.experimental.runners.Enclosed"; public static final String ORG_JUNIT_JUPITER_API_IO_TEMPDIR = "org.junit.jupiter.api.io.TempDir"; + public static final String ORG_JUNIT_PLATFORM_SUITE_API_SUITE = "org.junit.platform.suite.api.Suite"; + // junit 6 + public static final String ORG_JUNIT_JUPITER_API_METHOD_ORDERER_DEFAULT = "org.junit.jupiter.api.MethodOrderer.Default"; } diff --git a/java/java-impl/src/com/intellij/testIntegration/JavaTestFramework.java b/java/java-impl/src/com/intellij/testIntegration/JavaTestFramework.java index 63dd3873587a..865371fbc43c 100644 --- a/java/java-impl/src/com/intellij/testIntegration/JavaTestFramework.java +++ b/java/java-impl/src/com/intellij/testIntegration/JavaTestFramework.java @@ -30,6 +30,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.concurrency.Promise; import org.jetbrains.concurrency.Promises; +import java.util.Collection; import java.util.Collections; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentMap; @@ -42,15 +43,19 @@ public abstract class JavaTestFramework implements JvmTestFramework { public boolean isLibraryAttached(@NotNull Module module) { GlobalSearchScope scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module); Project project = module.getProject(); - PsiClass c = DumbService.getInstance(project).computeWithAlternativeResolveEnabled(() -> JavaPsiFacade.getInstance(project).findClass(getMarkerClassFQName(), scope)); - return c != null; + return DumbService.getInstance(project).computeWithAlternativeResolveEnabled(() -> { + for (String markerClassFQName : getMarkerClassFQNames()) { + if (JavaPsiFacade.getInstance(project).findClass(markerClassFQName, scope) != null) return true; + } + return false; + }); } @Override public @Nullable String getLibraryPath() { ExternalLibraryDescriptor descriptor = getFrameworkLibraryDescriptor(); if (descriptor != null) { - return descriptor.getLibraryClassesRoots().get(0); + return descriptor.getLibraryClassesRoots().getFirst(); } return null; } @@ -60,14 +65,24 @@ public abstract class JavaTestFramework implements JvmTestFramework { return null; } + protected Collection getMarkerClassFQNames() { + return Collections.singleton(getMarkerClassFQName()); + } + + /** + * @deprecated Use {@link #getMarkerClassFQNames()} instead + */ + @Deprecated protected abstract String getMarkerClassFQName(); /** - * Return {@code true} iff {@link #getMarkerClassFQName()} can be found in the resolve scope of {@code clazz} + * Return {@code true} iff {@link #getMarkerClassFQNames()} can be found in the resolve scope of {@code clazz} */ public boolean isFrameworkAvailable(@NotNull PsiElement clazz) { - String markerClassFQName = getMarkerClassFQName(); - return isFrameworkApplicable(clazz, markerClassFQName); + for (String markerClassFQName : getMarkerClassFQNames()) { + if (isFrameworkApplicable(clazz, markerClassFQName)) return true; + } + return false; } protected static boolean isFrameworkApplicable(@NotNull PsiElement clazz, String markerClassFQName) { diff --git a/lib/BUILD.bazel b/lib/BUILD.bazel index a2d59a5400c2..e5ede940a276 100644 --- a/lib/BUILD.bazel +++ b/lib/BUILD.bazel @@ -3246,6 +3246,72 @@ java_library( visibility = ["//visibility:public"] ) +java_library( + name = "junit6", + exports = [ + ":junit-jupiter-api-6_0_0-RC2_http_import", + ":opentest4j-1_3_0_http_import", + ":junit-platform-commons-6_0_0-RC2_http_import", + ":apiguardian-api-1_1_2_http_import", + ":jspecify-1_0_0_http_import", + ], + visibility = ["//visibility:public"] +) + +jvm_import( + name = "junit-jupiter-api-6_0_0-RC2_http_import", + jar = "@junit-jupiter-api-6_0_0-RC2_http//file", + source_jar = "@junit-jupiter-api-6_0_0-RC2-sources_http//file" +) + +jvm_import( + name = "junit-platform-commons-6_0_0-RC2_http_import", + jar = "@junit-platform-commons-6_0_0-RC2_http//file", + source_jar = "@junit-platform-commons-6_0_0-RC2-sources_http//file" +) + +jvm_import( + name = "jspecify-1_0_0_http_import", + jar = "@jspecify-1_0_0_http//file", + source_jar = "@jspecify-1_0_0-sources_http//file" +) + +java_library( + name = "junit6-provided", + exports = [":junit6"], + neverlink = True, + visibility = ["//visibility:public"] +) + +java_library( + name = "junit6Launcher", + exports = [ + ":junit-platform-launcher-6_0_0-RC2_http_import", + ":junit-platform-engine-6_0_0-RC2_http_import", + ":jspecify-1_0_0_http_import", + ], + visibility = ["//visibility:public"] +) + +jvm_import( + name = "junit-platform-launcher-6_0_0-RC2_http_import", + jar = "@junit-platform-launcher-6_0_0-RC2_http//file", + source_jar = "@junit-platform-launcher-6_0_0-RC2-sources_http//file" +) + +jvm_import( + name = "junit-platform-engine-6_0_0-RC2_http_import", + jar = "@junit-platform-engine-6_0_0-RC2_http//file", + source_jar = "@junit-platform-engine-6_0_0-RC2-sources_http//file" +) + +java_library( + name = "junit6Launcher-provided", + exports = [":junit6Launcher"], + neverlink = True, + visibility = ["//visibility:public"] +) + jvm_import( name = "jvm-native-trusted-roots", jar = "@jvm-native-trusted-roots-1_1_7_http//file", @@ -4557,12 +4623,6 @@ jvm_import( source_jar = "@lifecycle-common-jvm-2_9_2-sources_http//file" ) -jvm_import( - name = "jspecify-1_0_0_http_import", - jar = "@jspecify-1_0_0_http//file", - source_jar = "@jspecify-1_0_0-sources_http//file" -) - jvm_import( name = "lifecycle-runtime-desktop-2_9_2_http_import", jar = "@lifecycle-runtime-desktop-2_9_2_http//file", diff --git a/lib/MODULE.bazel b/lib/MODULE.bazel index 211b26f7406f..ddd0374dcf6c 100644 --- a/lib/MODULE.bazel +++ b/lib/MODULE.bazel @@ -4605,6 +4605,76 @@ http_file( downloaded_file_path = "junit-vintage-engine-5.13.4-sources.jar" ) +http_file( + name = "junit-jupiter-api-6_0_0-RC2_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-api/6.0.0-RC2/junit-jupiter-api-6.0.0-RC2.jar", + sha256 = "db1404414d23effbeb1202025f1e984f9069ddeed6d4c1826bb70bce8c70fb31", + downloaded_file_path = "junit-jupiter-api-6.0.0-RC2.jar" +) + +http_file( + name = "junit-platform-commons-6_0_0-RC2_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/junit/platform/junit-platform-commons/6.0.0-RC2/junit-platform-commons-6.0.0-RC2.jar", + sha256 = "7f7d65576d02547d4d6b44ec3d49dd37904cd8927baae5bddd541cd24d513a39", + downloaded_file_path = "junit-platform-commons-6.0.0-RC2.jar" +) + +http_file( + name = "jspecify-1_0_0_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar", + sha256 = "1fad6e6be7557781e4d33729d49ae1cdc8fdda6fe477bb0cc68ce351eafdfbab", + downloaded_file_path = "jspecify-1.0.0.jar" +) + +http_file( + name = "junit-jupiter-api-6_0_0-RC2-sources_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-api/6.0.0-RC2/junit-jupiter-api-6.0.0-RC2-sources.jar", + sha256 = "4addc5bf25b7ecdd2b24b2710632fca9854410cc96d5e2332c0ac3b123ebb080", + downloaded_file_path = "junit-jupiter-api-6.0.0-RC2-sources.jar" +) + +http_file( + name = "junit-platform-commons-6_0_0-RC2-sources_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/junit/platform/junit-platform-commons/6.0.0-RC2/junit-platform-commons-6.0.0-RC2-sources.jar", + sha256 = "e1469584f6c28fcf4eaf44dc3fd300bbeb51912f4052edbef7ee5ee0a28ff744", + downloaded_file_path = "junit-platform-commons-6.0.0-RC2-sources.jar" +) + +http_file( + name = "jspecify-1_0_0-sources_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/jspecify/jspecify/1.0.0/jspecify-1.0.0-sources.jar", + sha256 = "adf0898191d55937fb3192ba971826f4f294292c4a960740f3c27310e7b70296", + downloaded_file_path = "jspecify-1.0.0-sources.jar" +) + +http_file( + name = "junit-platform-launcher-6_0_0-RC2_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/junit/platform/junit-platform-launcher/6.0.0-RC2/junit-platform-launcher-6.0.0-RC2.jar", + sha256 = "3c41ec667a185ec0b51ca8d9d68e5f0e425d6645c6e4eb64b8a81716a4deaed1", + downloaded_file_path = "junit-platform-launcher-6.0.0-RC2.jar" +) + +http_file( + name = "junit-platform-engine-6_0_0-RC2_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/junit/platform/junit-platform-engine/6.0.0-RC2/junit-platform-engine-6.0.0-RC2.jar", + sha256 = "8f5e8cb552c311d4a236334975a5ff9ec4db5f64e84151d01500f31fa19edfba", + downloaded_file_path = "junit-platform-engine-6.0.0-RC2.jar" +) + +http_file( + name = "junit-platform-launcher-6_0_0-RC2-sources_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/junit/platform/junit-platform-launcher/6.0.0-RC2/junit-platform-launcher-6.0.0-RC2-sources.jar", + sha256 = "f100a0b9b4249e8f050375b47875b91dba01c41fa226dfb31c94c3bdc9de0f72", + downloaded_file_path = "junit-platform-launcher-6.0.0-RC2-sources.jar" +) + +http_file( + name = "junit-platform-engine-6_0_0-RC2-sources_http", + url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/junit/platform/junit-platform-engine/6.0.0-RC2/junit-platform-engine-6.0.0-RC2-sources.jar", + sha256 = "6b725abf25a6688bf413980ab07ecb2423cf093fea08a607eef01afa7f6a457d", + downloaded_file_path = "junit-platform-engine-6.0.0-RC2-sources.jar" +) + http_file( name = "jvm-native-trusted-roots-1_1_7_http", url = "https://cache-redirector.jetbrains.com/packages.jetbrains.team/maven/p/ij/intellij-dependencies/org/jetbrains/nativecerts/jvm-native-trusted-roots/1.1.7/jvm-native-trusted-roots-1.1.7.jar", @@ -6418,13 +6488,6 @@ http_file( downloaded_file_path = "lifecycle-common-jvm-2.9.2.jar" ) -http_file( - name = "jspecify-1_0_0_http", - url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar", - sha256 = "1fad6e6be7557781e4d33729d49ae1cdc8fdda6fe477bb0cc68ce351eafdfbab", - downloaded_file_path = "jspecify-1.0.0.jar" -) - http_file( name = "lifecycle-runtime-desktop-2_9_2_http", url = "https://cache-redirector.jetbrains.com/dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime-desktop/2.9.2/lifecycle-runtime-desktop-2.9.2.jar", @@ -6586,13 +6649,6 @@ http_file( downloaded_file_path = "lifecycle-common-jvm-2.9.2-sources.jar" ) -http_file( - name = "jspecify-1_0_0-sources_http", - url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/org/jspecify/jspecify/1.0.0/jspecify-1.0.0-sources.jar", - sha256 = "adf0898191d55937fb3192ba971826f4f294292c4a960740f3c27310e7b70296", - downloaded_file_path = "jspecify-1.0.0-sources.jar" -) - http_file( name = "lifecycle-runtime-desktop-2_9_2-sources_http", url = "https://cache-redirector.jetbrains.com/dl.google.com/dl/android/maven2/androidx/lifecycle/lifecycle-runtime-desktop/2.9.2/lifecycle-runtime-desktop-2.9.2-sources.jar", diff --git a/platform/bootstrap/dev/src/org/jetbrains/intellij/build/devServer/JUnitDevMainKt.java b/platform/bootstrap/dev/src/org/jetbrains/intellij/build/devServer/JUnitDevMainKt.java index f5e5c93fee97..72c40becfa06 100644 --- a/platform/bootstrap/dev/src/org/jetbrains/intellij/build/devServer/JUnitDevMainKt.java +++ b/platform/bootstrap/dev/src/org/jetbrains/intellij/build/devServer/JUnitDevMainKt.java @@ -10,7 +10,9 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.nio.file.Path; import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Arrays; import java.util.Collection; +import java.util.List; // in java - don't use kotlin to avoid loading non-JDK classes @ApiStatus.Internal @@ -84,21 +86,15 @@ public final class JUnitDevMainKt { String result = null; - String junitRtJar = File.separator + "junit-rt.jar"; - String junitRtPackage = File.separator + "intellij.junit.rt"; - for (String s : testEntryPointClasspath.split(File.pathSeparator)) { - if (s.endsWith(junitRtJar) || s.endsWith(junitRtPackage)) { - result = s; - break; - } - } - - String junit5RtJar = File.separator + "junit5-rt.jar"; - String junit5RtPackage = File.separator + "intellij.junit.v5.rt"; - for (String s : testEntryPointClasspath.split(File.pathSeparator)) { - if (s.endsWith(junit5RtJar) || s.endsWith(junit5RtPackage)) { - result = result == null ? s : result + File.pathSeparator + s; - break; + List versions = Arrays.asList("", "5", "6"); + for (String version : versions) { + String junitRtJar = File.separator + "junit" + version + "-rt.jar"; + String junitRtPackage = File.separator + "intellij.junit" + (version.isEmpty() ? "" : ".v" + version) + ".rt"; + for (String s : testEntryPointClasspath.split(File.pathSeparator)) { + if (s.endsWith(junitRtJar) || s.endsWith(junitRtPackage)) { + result = result == null ? s : result + File.pathSeparator + s; + break; + } } } diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityRepositoryModules.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityRepositoryModules.kt index 7cba4dfa1924..35dca1b359e3 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityRepositoryModules.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityRepositoryModules.kt @@ -154,6 +154,7 @@ object CommunityRepositoryModules { pluginAuto("intellij.junit") { spec -> spec.withModule("intellij.junit.rt", "junit-rt.jar") spec.withModule("intellij.junit.v5.rt", "junit5-rt.jar") + spec.withModule("intellij.junit.v6.rt", "junit6-rt.jar") }, plugin("intellij.testng") { spec -> spec.mainJarName = "testng-plugin.jar" diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/newvfs/impl/PersistentFS_FindFilesTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/newvfs/impl/PersistentFS_FindFilesTest.java index b454327792da..25a3b16fe22f 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/newvfs/impl/PersistentFS_FindFilesTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/newvfs/impl/PersistentFS_FindFilesTest.java @@ -318,7 +318,6 @@ public class PersistentFS_FindFilesTest { } finally { pool.shutdown(); - //noinspection ResultOfMethodCallIgnored pool.awaitTermination(1, MINUTES); } } diff --git a/platform/testFramework/testResources/tests/testGroups.properties b/platform/testFramework/testResources/tests/testGroups.properties index 8a8ff66bb2d5..c97180fb33a6 100644 --- a/platform/testFramework/testResources/tests/testGroups.properties +++ b/platform/testFramework/testResources/tests/testGroups.properties @@ -371,6 +371,7 @@ de.plushnikov.intellij.plugin.* com.intellij.junit4.* com.intellij.testng.integration.* com.intellij.testDiscovery.* +com.intellij.execution.junit.* [PYTHON_TESTS] com.jetbrains.python.* diff --git a/plugins/junit/java-tests/test/com/intellij/execution/junit/configuration/JUnitModulePathTest.java b/plugins/junit/java-tests/test/com/intellij/execution/junit/configuration/JUnitModulePathTest.java index 52bab16da0f7..7dcb254d0a2a 100644 --- a/plugins/junit/java-tests/test/com/intellij/execution/junit/configuration/JUnitModulePathTest.java +++ b/plugins/junit/java-tests/test/com/intellij/execution/junit/configuration/JUnitModulePathTest.java @@ -108,7 +108,7 @@ public class JUnitModulePathTest extends BaseConfigurationTestCase { "modulePath/prod1", module); ModuleRootModificationUtil.updateModel(module, model -> { ContentEntry entry = model.getContentEntries()[0]; - entry.removeSourceFolder(entry.getSourceFolders(JavaSourceRootType.SOURCE).get(0)); + entry.removeSourceFolder(entry.getSourceFolders(JavaSourceRootType.SOURCE).getFirst()); }); JavaParameters params4Tests = configuration.getTestObject().createJavaParameters4Tests(); assertEmpty(params4Tests.getModulePath().getPathList()); @@ -128,7 +128,7 @@ public class JUnitModulePathTest extends BaseConfigurationTestCase { PathsList modulePath = params4Tests.getModulePath(); assertTrue("module path: " + modulePath.getPathsString(), - modulePath.getPathList().stream().anyMatch(filePath -> filePath.contains("junit-jupiter-api"))); + ContainerUtil.exists(modulePath.getPathList(), filePath -> filePath.contains("junit-jupiter-api"))); //production module output is not on the module path assertFalse("module path: " + modulePath.getPathsString(), modulePath.getPathList().contains(getCompilerOutputPath(module))); @@ -138,7 +138,7 @@ public class JUnitModulePathTest extends BaseConfigurationTestCase { assertSize(2, modulePath.getPathList()); //launcher should be put on the classpath - assertTrue(params4Tests.getClassPath().getPathList().stream().anyMatch(filePath -> filePath.contains("launcher"))); + assertTrue(ContainerUtil.exists(params4Tests.getClassPath().getPathList(), filePath -> filePath.contains("launcher"))); } public void testModuleInfoInTestModularizedJunit() throws Exception { @@ -153,8 +153,8 @@ public class JUnitModulePathTest extends BaseConfigurationTestCase { " --add-modules org.junit.platform.launcher", moduleOptions.getParametersList().getParametersString()); PathsList classPath = params4Tests.getClassPath(); - assertContainsElements(classPath.getPathList(), PathUtil.getJarPathForClass(JUnitStarter.class)); - assertContainsElements(classPath.getPathList(), ContainerUtil.map(TestObject.getJUnit5RtFiles(), File::getPath)); + assertContainsElements(classPath.getPathList(), TestObject.getJUnitRtPath().getPath()); + assertContainsElements(classPath.getPathList(), ContainerUtil.map(TestObject.getJUnitRtFiles(JUnitStarter.JUNIT5_PARAMETER), File::getPath)); PathsList modulePath = params4Tests.getModulePath(); checkLibrariesOnPathList(module, modulePath); @@ -165,7 +165,7 @@ public class JUnitModulePathTest extends BaseConfigurationTestCase { //test module output is on the module path assertTrue("module path: " + modulePath.getPathsString(), modulePathList.contains(getCompilerOutputPath(module, true))); - assertTrue(modulePathList.stream().anyMatch(filePath -> filePath.contains("launcher"))); + assertTrue(ContainerUtil.exists(modulePathList, filePath -> filePath.contains("launcher"))); } public void testModuleInfoInTestModularizedJunitDependencyOnAnotherModule() throws Exception { @@ -176,7 +176,7 @@ public class JUnitModulePathTest extends BaseConfigurationTestCase { model.addModuleOrderEntry(module1); }); JpsMavenRepositoryLibraryDescriptor nonModularizedJupiterDescription = - new JpsMavenRepositoryLibraryDescriptor("org.junit.jupiter", "junit-jupiter-api", "5.5.2"); + new JpsMavenRepositoryLibraryDescriptor("org.junit.jupiter", "junit-jupiter-api", "6.0.0-RC2"); JUnitConfiguration configuration = setupConfiguration(nonModularizedJupiterDescription, "modulePath/test2", module); JavaParameters params4Tests = configuration.getTestObject().createJavaParameters4Tests(); ParamsGroup moduleOptions = JavaTestFrameworkRunnableState.getJigsawOptions(params4Tests); @@ -185,8 +185,8 @@ public class JUnitModulePathTest extends BaseConfigurationTestCase { " --add-modules org.junit.platform.launcher", moduleOptions.getParametersList().getParametersString()); PathsList classPath = params4Tests.getClassPath(); - assertContainsElements(classPath.getPathList(), PathUtil.getJarPathForClass(JUnitStarter.class)); - assertContainsElements(classPath.getPathList(), ContainerUtil.map(TestObject.getJUnit5RtFiles(), File::getPath)); + assertContainsElements(classPath.getPathList(), TestObject.getJUnitRtPath().getPath()); + assertContainsElements(classPath.getPathList(), ContainerUtil.map(TestObject.getJUnitRtFiles(JUnitStarter.JUNIT6_PARAMETER), File::getPath)); PathsList modulePath = params4Tests.getModulePath(); checkLibrariesOnPathList(module, modulePath); @@ -198,7 +198,7 @@ public class JUnitModulePathTest extends BaseConfigurationTestCase { //test module output is on the module path assertTrue("module path: " + modulePath.getPathsString(), modulePathList.contains(getCompilerOutputPath(module, true))); - assertTrue(modulePathList.stream().anyMatch(filePath -> filePath.contains("launcher"))); + assertTrue(ContainerUtil.exists(modulePathList, filePath -> filePath.contains("launcher"))); } private JUnitConfiguration setupConfiguration(JpsMavenRepositoryLibraryDescriptor libraryDescriptor, String sources, Module module) throws Exception { diff --git a/plugins/junit/plugin-content.yaml b/plugins/junit/plugin-content.yaml index c871e41589c6..fa6ef51d6eb6 100644 --- a/plugins/junit/plugin-content.yaml +++ b/plugins/junit/plugin-content.yaml @@ -6,4 +6,7 @@ - name: intellij.junit - name: lib/junit5-rt.jar modules: - - name: intellij.junit.v5.rt \ No newline at end of file + - name: intellij.junit.v5.rt +- name: lib/junit6-rt.jar + modules: + - name: intellij.junit.v6.rt \ No newline at end of file diff --git a/plugins/junit/resources/META-INF/plugin.xml b/plugins/junit/resources/META-INF/plugin.xml index c516ec0565a6..159f977756a2 100644 --- a/plugins/junit/resources/META-INF/plugin.xml +++ b/plugins/junit/resources/META-INF/plugin.xml @@ -28,7 +28,8 @@ - + + @@ -49,6 +50,7 @@ + diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 AfterAll Method.java.ft b/plugins/junit/resources/fileTemplates/code/JUnit6 AfterAll Method.java.ft new file mode 100644 index 000000000000..9f25d971a772 --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 AfterAll Method.java.ft @@ -0,0 +1,4 @@ +@org.junit.jupiter.api.AfterAll +static void afterAll() { + ${BODY} +} \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 AfterAll Method.java.html b/plugins/junit/resources/fileTemplates/code/JUnit6 AfterAll Method.java.html new file mode 100644 index 000000000000..28871975d466 --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 AfterAll Method.java.html @@ -0,0 +1,27 @@ + + + + + + + +
+ Creates an afterAll method in a JUnit 6 test class. +
+ + + + + + + + + + + + + + +
Predefined variables take the following values:
${NAME} Name of the created method
${BODY} Generated method body
+ + \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 BeforeAll Method.java.ft b/plugins/junit/resources/fileTemplates/code/JUnit6 BeforeAll Method.java.ft new file mode 100644 index 000000000000..3d6ea2541b0e --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 BeforeAll Method.java.ft @@ -0,0 +1,4 @@ +@org.junit.jupiter.api.BeforeAll +static void beforeAll() { + ${BODY} +} \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 BeforeAll Method.java.html b/plugins/junit/resources/fileTemplates/code/JUnit6 BeforeAll Method.java.html new file mode 100644 index 000000000000..fc040285bc1b --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 BeforeAll Method.java.html @@ -0,0 +1,27 @@ + + + + + + + +
+ Creates a beforeAll method in a JUnit 6 test class. +
+ + + + + + + + + + + + + + +
Predefined variables take the following values:
${NAME} Name of the created method
${BODY} generated method body
+ + \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 SetUp Method.java.ft b/plugins/junit/resources/fileTemplates/code/JUnit6 SetUp Method.java.ft new file mode 100644 index 000000000000..2d190faed16e --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 SetUp Method.java.ft @@ -0,0 +1,4 @@ +@org.junit.jupiter.api.BeforeEach +void setUp() { + ${BODY} +} \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 SetUp Method.java.html b/plugins/junit/resources/fileTemplates/code/JUnit6 SetUp Method.java.html new file mode 100644 index 000000000000..69eebbf8c746 --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 SetUp Method.java.html @@ -0,0 +1,26 @@ + + + + + + +
+ Creates a setUp method in a JUnit 6 test class. +
+ + + + + + + + + + + + + + +
Predefined variables take the following values:
${NAME} Name of the created method
${BODY} Generated method body
+ + \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 TearDown Method.java.ft b/plugins/junit/resources/fileTemplates/code/JUnit6 TearDown Method.java.ft new file mode 100644 index 000000000000..2427c42d8c11 --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 TearDown Method.java.ft @@ -0,0 +1,4 @@ +@org.junit.jupiter.api.AfterEach +void tearDown() { + ${BODY} +} \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 TearDown Method.java.html b/plugins/junit/resources/fileTemplates/code/JUnit6 TearDown Method.java.html new file mode 100644 index 000000000000..1f6ae677c80a --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 TearDown Method.java.html @@ -0,0 +1,26 @@ + + + + + + +
+ Creates a tearDown method in a JUnit 6 test class. +
+ + + + + + + + + + + + + + +
Predefined variables take the following values:
${NAME} Name of the created method
${BODY} Generated method body
+ + \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 Test Class.java.ft b/plugins/junit/resources/fileTemplates/code/JUnit6 Test Class.java.ft new file mode 100644 index 000000000000..5d644a003cb4 --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 Test Class.java.ft @@ -0,0 +1,5 @@ +import static org.junit.jupiter.api.Assertions.*; +#parse("File Header.java") +class ${NAME} { + ${BODY} +} \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 Test Class.java.html b/plugins/junit/resources/fileTemplates/code/JUnit6 Test Class.java.html new file mode 100644 index 000000000000..62b218efa327 --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 Test Class.java.html @@ -0,0 +1,31 @@ + + + + + + +
+ Creates a JUnit 6 test class. +
+ + + + + + + + + + + + + + + + + + + +
Predefined variables take the following values:
${NAME} Name of the created class
${CLASS_NAME} Name of the tested class
${BODY} Generated class body
+ + \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 Test Method.java.ft b/plugins/junit/resources/fileTemplates/code/JUnit6 Test Method.java.ft new file mode 100644 index 000000000000..b2491497aef4 --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 Test Method.java.ft @@ -0,0 +1,4 @@ +@org.junit.jupiter.api.Test +void ${NAME}() { + ${BODY} +} \ No newline at end of file diff --git a/plugins/junit/resources/fileTemplates/code/JUnit6 Test Method.java.html b/plugins/junit/resources/fileTemplates/code/JUnit6 Test Method.java.html new file mode 100644 index 000000000000..55309764a9fd --- /dev/null +++ b/plugins/junit/resources/fileTemplates/code/JUnit6 Test Method.java.html @@ -0,0 +1,26 @@ + + + + + + +
+ Creates a test method in a JUnit 6 test class. +
+ + + + + + + + + + + + + + +
Predefined variables take the following values:
${NAME} Name of the created method
${BODY} Generated method body
+ + \ No newline at end of file diff --git a/plugins/junit/src/com/intellij/execution/junit/JUnit5Framework.java b/plugins/junit/src/com/intellij/execution/junit/JUnit5Framework.java index a9ea6748876f..57657e027a7f 100644 --- a/plugins/junit/src/com/intellij/execution/junit/JUnit5Framework.java +++ b/plugins/junit/src/com/intellij/execution/junit/JUnit5Framework.java @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.execution.junit; import com.intellij.codeInsight.AnnotationUtil; @@ -16,6 +16,11 @@ import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Collection; +import java.util.List; + +import static com.siyeh.ig.junit.JUnitCommonClassNames.ORG_JUNIT_PLATFORM_SUITE_API_SUITE; + public class JUnit5Framework extends JUnitTestFramework { @Override @@ -29,17 +34,13 @@ public class JUnit5Framework extends JUnitTestFramework { } @Override - protected String getMarkerClassFQName() { - return JUnitUtil.TEST5_ANNOTATION; + protected Collection getMarkerClassFQNames() { + return List.of(JUnitUtil.TEST5_ANNOTATION, JUnitUtil.CUSTOM_TESTABLE_ANNOTATION); } @Override - public boolean isFrameworkAvailable(@NotNull PsiElement clazz) { - return callWithAlternateResolver(clazz.getProject(), ()->{ - return isFrameworkApplicable(clazz, JUnitUtil.CUSTOM_TESTABLE_ANNOTATION) || - //check explicit jupiter to support library with broken dependency - isFrameworkApplicable(clazz, JUnitUtil.TEST5_ANNOTATION); - }, false); + protected String getMarkerClassFQName() { + return JUnitUtil.TEST5_ANNOTATION; } @Override @@ -196,7 +197,7 @@ public class JUnit5Framework extends JUnitTestFramework { public boolean isSuiteClass(PsiClass psiClass) { if (psiClass == null) return false; return callWithAlternateResolver(psiClass.getProject(), () -> { - return AnnotationUtil.isAnnotated(psiClass, "org.junit.platform.suite.api.Suite", AnnotationUtil.CHECK_HIERARCHY); + return AnnotationUtil.isAnnotated(psiClass, ORG_JUNIT_PLATFORM_SUITE_API_SUITE, AnnotationUtil.CHECK_HIERARCHY); }, false); } } diff --git a/plugins/junit/src/com/intellij/execution/junit/JUnit6Framework.java b/plugins/junit/src/com/intellij/execution/junit/JUnit6Framework.java new file mode 100644 index 000000000000..fa7c6fd1593a --- /dev/null +++ b/plugins/junit/src/com/intellij/execution/junit/JUnit6Framework.java @@ -0,0 +1,204 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.execution.junit; + +import com.intellij.codeInsight.AnnotationUtil; +import com.intellij.codeInsight.intention.AddAnnotationPsiFix; +import com.intellij.execution.JUnitBundle; +import com.intellij.ide.fileTemplates.FileTemplateDescriptor; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ExternalLibraryDescriptor; +import com.intellij.openapi.ui.Messages; +import com.intellij.psi.*; +import com.intellij.psi.codeStyle.JavaCodeStyleManager; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +import static com.siyeh.ig.junit.JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_METHOD_ORDERER_DEFAULT; +import static com.siyeh.ig.junit.JUnitCommonClassNames.ORG_JUNIT_PLATFORM_SUITE_API_SUITE; + +public class JUnit6Framework extends JUnitTestFramework { + + @Override + public boolean isDumbAware() { + return this.getClass().isAssignableFrom(JUnit6Framework.class); + } + + @Override + public @NotNull String getName() { + return "JUnit6"; + } + + @Override + protected Collection getMarkerClassFQNames() { + return List.of(ORG_JUNIT_JUPITER_API_METHOD_ORDERER_DEFAULT, "org.junit.platform.commons.annotation.Contract"); + } + + @Override + protected String getMarkerClassFQName() { + return ORG_JUNIT_JUPITER_API_METHOD_ORDERER_DEFAULT; + } + + @Override + public boolean shouldRunSingleClassAsJUnit5(Project project, GlobalSearchScope scope) { + return true; + } + + @Override + public @Nullable ExternalLibraryDescriptor getFrameworkLibraryDescriptor() { + return JUnitExternalLibraryDescriptor.JUNIT6; + } + + @Override + public @Nullable String getDefaultSuperClass() { + return null; + } + + @Override + public boolean isTestClass(PsiClass clazz, boolean canBePotential) { + return callWithAlternateResolver(clazz.getProject(), () -> { + if (canBePotential) return isUnderTestSources(clazz); + if (!isFrameworkAvailable(clazz)) return false; + return JUnitUtil.isJUnit5TestClass(clazz, false); + }, false); + } + + @Override + protected @Nullable PsiMethod findSetUpMethod(@NotNull PsiClass clazz) { + return callWithAlternateResolver(clazz.getProject(), ()->{ + for (PsiMethod each : clazz.getMethods()) { + if (AnnotationUtil.isAnnotated(each, JUnitUtil.BEFORE_EACH_ANNOTATION_NAME, 0)) return each; + } + return null; + }, null); + } + + @Override + protected @Nullable PsiMethod findBeforeClassMethod(@NotNull PsiClass clazz) { + return callWithAlternateResolver(clazz.getProject(), () -> { + for (PsiMethod each : clazz.getMethods()) { + if (each.hasModifierProperty(PsiModifier.STATIC) + && AnnotationUtil.isAnnotated(each, JUnitUtil.BEFORE_ALL_ANNOTATION_NAME, 0)) return each; + } + return null; + }, null); + } + + @Override + protected @Nullable PsiMethod findTearDownMethod(@NotNull PsiClass clazz) { + return callWithAlternateResolver(clazz.getProject(), () -> { + for (PsiMethod each : clazz.getMethods()) { + if (AnnotationUtil.isAnnotated(each, JUnitUtil.AFTER_EACH_ANNOTATION_NAME, 0)) return each; + } + return null; + }, null); + } + + @Override + protected @Nullable PsiMethod findAfterClassMethod(@NotNull PsiClass clazz) { + return callWithAlternateResolver(clazz.getProject(), () -> { + for (PsiMethod each : clazz.getMethods()) { + if (each.hasModifierProperty(PsiModifier.STATIC) + && AnnotationUtil.isAnnotated(each, JUnitUtil.AFTER_ALL_ANNOTATION_NAME, 0)) { + return each; + } + } + return null; + }, null); + } + + @Override + protected @Nullable PsiMethod findOrCreateSetUpMethod(PsiClass clazz) throws IncorrectOperationException { + PsiMethod method = findSetUpMethod(clazz); + if (method != null) return method; + + PsiManager manager = clazz.getManager(); + PsiElementFactory factory = JavaPsiFacade.getInstance(manager.getProject()).getElementFactory(); + + method = createSetUpPatternMethod(factory); + PsiMethod existingMethod = clazz.findMethodBySignature(method, false); + if (existingMethod != null) { + if (AnnotationUtil.isAnnotated(existingMethod, JUnitUtil.BEFORE_ALL_ANNOTATION_NAME, 0)) return existingMethod; + int exit = ApplicationManager.getApplication().isUnitTestMode() ? + Messages.OK : + Messages.showOkCancelDialog(JUnitBundle.message("create.setup.dialog.message", "@BeforeEach"), + JUnitBundle.message("create.setup.dialog.title"), + Messages.getWarningIcon()); + if (exit == Messages.OK) { + AddAnnotationPsiFix.addPhysicalAnnotationIfAbsent(JUnitUtil.BEFORE_EACH_ANNOTATION_NAME, PsiNameValuePair.EMPTY_ARRAY, + existingMethod.getModifierList()); + return existingMethod; + } + } + final PsiMethod testMethod = JUnitUtil.findFirstTestMethod(clazz); + if (testMethod != null) { + method = (PsiMethod)clazz.addBefore(method, testMethod); + } else { + method = (PsiMethod)clazz.add(method); + } + JavaCodeStyleManager.getInstance(manager.getProject()).shortenClassReferences(method); + + return method; + } + + @Override + public boolean isIgnoredMethod(PsiElement element) { + if (element == null) return false; + return callWithAlternateResolver(element.getProject(), () -> { + if (element instanceof PsiMethod method) { + final PsiMethod ignoredTestMethod = + AnnotationUtil.isAnnotated(method, "org.junit.jupiter.api.Disabled", 0) ? JUnitUtil.getTestMethod(element) : null; + return ignoredTestMethod != null; + } + return false; + }, true); + } + + @Override + public boolean acceptNestedClasses() { + return true; + } + + @Override + public FileTemplateDescriptor getSetUpMethodFileTemplateDescriptor() { + return new FileTemplateDescriptor("JUnit6 SetUp Method.java"); + } + + @Override + public FileTemplateDescriptor getBeforeClassMethodFileTemplateDescriptor() { + return new FileTemplateDescriptor("JUnit6 BeforeAll Method.java"); + } + + @Override + public FileTemplateDescriptor getTearDownMethodFileTemplateDescriptor() { + return new FileTemplateDescriptor("JUnit6 TearDown Method.java"); + } + + @Override + public FileTemplateDescriptor getAfterClassMethodFileTemplateDescriptor() { + return new FileTemplateDescriptor("JUnit6 AfterAll Method.java"); + } + + @Override + public @NotNull FileTemplateDescriptor getTestMethodFileTemplateDescriptor() { + return new FileTemplateDescriptor("JUnit6 Test Method.java"); + } + + @Override + public FileTemplateDescriptor getTestClassFileTemplateDescriptor() { + return new FileTemplateDescriptor("JUnit6 Test Class.java"); + } + + @Override + public boolean isSuiteClass(PsiClass psiClass) { + if (psiClass == null) return false; + return callWithAlternateResolver(psiClass.getProject(), () -> { + return AnnotationUtil.isAnnotated(psiClass, ORG_JUNIT_PLATFORM_SUITE_API_SUITE, AnnotationUtil.CHECK_HIERARCHY); + }, false); + } +} diff --git a/plugins/junit/src/com/intellij/execution/junit/JUnitExternalLibraryDescriptor.java b/plugins/junit/src/com/intellij/execution/junit/JUnitExternalLibraryDescriptor.java index f7481c404cc3..6c3884487ffb 100644 --- a/plugins/junit/src/com/intellij/execution/junit/JUnitExternalLibraryDescriptor.java +++ b/plugins/junit/src/com/intellij/execution/junit/JUnitExternalLibraryDescriptor.java @@ -1,25 +1,22 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.execution.junit; import com.intellij.openapi.roots.ExternalLibraryDescriptor; import org.jetbrains.annotations.NotNull; public final class JUnitExternalLibraryDescriptor extends ExternalLibraryDescriptor { - public static final ExternalLibraryDescriptor JUNIT3 = new JUnitExternalLibraryDescriptor("3", "3.8.2"); - public static final ExternalLibraryDescriptor JUNIT4 = new JUnitExternalLibraryDescriptor("4", "4.13.1"); - public static final ExternalLibraryDescriptor JUNIT5 = new JUnitExternalLibraryDescriptor("org.junit.jupiter", "junit-jupiter", "5.8.1", - "5.8.1"); + public static final ExternalLibraryDescriptor JUNIT3 = new JUnitExternalLibraryDescriptor("junit", "junit", "3", "3.8.2", "3.0"); + public static final ExternalLibraryDescriptor JUNIT4 = new JUnitExternalLibraryDescriptor("junit", "junit", "4", "4.13.1", "4.0"); + public static final ExternalLibraryDescriptor JUNIT5 = new JUnitExternalLibraryDescriptor("org.junit.jupiter", "junit-jupiter", "5", "5.13.4", "5.0.0"); + public static final ExternalLibraryDescriptor JUNIT6 = new JUnitExternalLibraryDescriptor("org.junit.jupiter", "junit-jupiter", "6", "6.0.0-RC2", "6.0.0-RC2"); private final String myVersion; - private JUnitExternalLibraryDescriptor(String baseVersion, String preferredVersion) { - this("junit", "junit", baseVersion, preferredVersion); - } - - private JUnitExternalLibraryDescriptor(final String groupId, - final String artifactId, - final String version, - String preferredVersion) { - super(groupId, artifactId, version + ".0", version + ".999", preferredVersion); + private JUnitExternalLibraryDescriptor(@NotNull String groupId, + @NotNull String artifactId, + @NotNull String version, + @NotNull String preferredVersion, + @NotNull String minVersion) { + super(groupId, artifactId, minVersion, version + ".999", preferredVersion); myVersion = version; } diff --git a/plugins/junit/src/com/intellij/execution/junit/TestObject.java b/plugins/junit/src/com/intellij/execution/junit/TestObject.java index 87c48d26d409..799fc4da9a55 100644 --- a/plugins/junit/src/com/intellij/execution/junit/TestObject.java +++ b/plugins/junit/src/com/intellij/execution/junit/TestObject.java @@ -42,8 +42,7 @@ import com.intellij.openapi.util.NlsSafe; import com.intellij.openapi.util.ThrowableComputable; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.JarFileSystem; -import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.*; import com.intellij.psi.*; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; @@ -82,6 +81,10 @@ import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; +import static com.intellij.execution.junit.JUnitExternalLibraryDescriptor.JUNIT5; +import static com.intellij.execution.junit.JUnitExternalLibraryDescriptor.JUNIT6; +import static com.siyeh.ig.junit.JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_METHOD_ORDERER_DEFAULT; + public abstract class TestObject extends JavaTestFrameworkRunnableState implements PossiblyDumbAware { private static final String LAUNCHER_MODULE_NAME = "org.junit.platform.launcher"; private static final String JUPITER_ENGINE_NAME = "org.junit.jupiter.engine"; @@ -100,6 +103,14 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState myAdditionalJarsForModuleFork = new HashMap<>(); + private static final Map RUNNER_VERSIONS = Map.of( + JUnitStarter.JUNIT3_PARAMETER, "3", + JUnitStarter.JUNIT4_PARAMETER, "4", + JUnitStarter.JUNIT5_PARAMETER, "5", + JUnitStarter.JUNIT6_PARAMETER, "6" + ); + protected static final Set JUPITER_RUNNERS = Set.of(JUnitStarter.JUNIT5_PARAMETER, JUnitStarter.JUNIT6_PARAMETER); + protected TestObject(JUnitConfiguration configuration, ExecutionEnvironment environment) { super(environment); myConfiguration = configuration; @@ -180,7 +191,7 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState result = new HashSet<>(); final SourceScope sourceScope = getSourceScope(); final Project project = getConfiguration().getProject(); - if (sourceScope != null && packageName != null && JUnitStarter.JUNIT5_PARAMETER.equals(getRunner())) { + if (sourceScope != null && packageName != null && JUPITER_RUNNERS.contains(getRunner())) { final PsiPackage aPackage = JavaPsiFacade.getInstance(getConfiguration().getProject()).findPackage(packageName); if (aPackage != null) { final TestSearchScope scope = getScope(); @@ -235,10 +246,12 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState getJUnit5RtFiles() { - File junit4Rt = new File(PathUtil.getJarPathForClass(JUnit4IdeaTestRunner.class)); + public static List getJUnitRtFiles(@NotNull String runner) { + String version = RUNNER_VERSIONS.getOrDefault(runner, "5"); + File junit4Rt = getJUnitRtPath(); String junit4Name = junit4Rt.getName(); - String junit5Name; + String junitCurrentName; if (junit4Rt.isDirectory()) { - junit5Name = junit4Name.replace("junit", "junit.v5"); + junitCurrentName = junit4Name.replace("junit", "junit.v" + version); } else { var relevantJarsRoot = ArchivedCompilationContextUtil.getArchivedCompiledClassesLocation(); Map> mapping = ArchivedCompilationContextUtil.getArchivedCompiledClassesMapping(); if (relevantJarsRoot != null && junit4Rt.toPath().startsWith(relevantJarsRoot) && mapping != null) { - return ContainerUtil.map(mapping.get("production/intellij.junit.v5.rt"), File::new); + return ContainerUtil.map(mapping.get("production/intellij.junit.v" + version + ".rt"), File::new); } else { - junit5Name = junit4Name.replace("junit", "junit5"); + junitCurrentName = junit4Name.replace("junit", "junit" + version); } } - return Collections.singletonList(new File(junit4Rt.getParent(), junit5Name)); + return Collections.singletonList(new File(junit4Rt.getParent(), junitCurrentName)); + } + + public static File getJUnitRtPath() { + String currentPath = PathUtil.getJarPathForClass(TestObject.class); + String currentUrl = VfsUtil.getUrlForLibraryRoot(new File(currentPath)); + if (StandardFileSystems.FILE_PROTOCOL.equals(VirtualFileManager.extractProtocol(currentUrl))) { + File rtDir = new File(new File(currentPath).getParentFile(), "intellij.junit.rt"); + if (rtDir.exists()) { + return rtDir; + } + } + return new File(PathUtil.getJarPathForClass(JUnit4IdeaTestRunner.class)); } /** - * Junit 5 searches for tests in the classpath. - * When 2 modules have e.g. the same package, one depends on another, and tests have to run in single module only, - * by configuration settings or to avoid repetition in fork by module mode, additional filters per output directories are required. + * Junit 5/6 searches for tests in the classpath. + * When 2 modules have e.g. the same package, one depends on another, and tests have to run in a single module only, + * by configuration settings or to avoid repetition in fork by module mode; additional filters per output directories are required. */ protected static @Unmodifiable List composeDirectoryFilter(@NotNull Module module) { return ContainerUtil.map(OrderEnumerator.orderEntries(module) @@ -296,6 +322,7 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState= 0; - if (isModularized) { //for modularized junit ensure launcher is included in the module graph + if (isModularized) { //for modularized junit ensure the launcher is included in the module graph ParamsGroup group = getOrCreateJigsawOptions(javaParameters); ParametersList vmParametersList = group.getParametersList(); if (!vmParametersList.hasParameter(LAUNCHER_MODULE_NAME)) { @@ -368,8 +395,12 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState= 0) { downloadDependenciesWhenRequired(project, additionalDependencies, new RepositoryLibraryProperties("org.junit.platform", "junit-platform-suite-engine", suiteVersion)); @@ -433,8 +464,8 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState * Reflection is needed for the case when no sources are attached */ @@ -452,7 +483,7 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState downloader = () -> { - appendJUnit5LauncherClasses(javaParameters, project, - getScopeForJUnit(module, project), - useModulePath() && module != null && ReadAction.compute(() -> findJavaModule(module, true)) != null); + appendJUnitLauncherClasses(preferredRunner, javaParameters, project, + getScopeForJUnit(module, project), + useModulePath() && module != null && ReadAction.compute(() -> findJavaModule(module, true)) != null); if (forkPerModule()) { for (Module packageModule : ReadAction.compute(() -> collectPackageModules(configuration.getPackage()))) { JavaParameters parameters = new JavaParameters(); @@ -587,12 +618,9 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState findJavaModule(packageModule, true)) != null); + appendJUnitLauncherClasses(preferredRunner, parameters, project, + getScopeForJUnit(packageModule, project), + useModulePath() && packageModule != null && ReadAction.compute(() -> findJavaModule(packageModule, true)) != null); myAdditionalJarsForModuleFork.put(packageModule, parameters); } } @@ -728,6 +756,20 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState testFrameworks = TestFrameworks.detectApplicableFrameworks(psiClass); TestFramework testFramework = ContainerUtil.getFirstItem(testFrameworks); + if (testFramework instanceof JUnit6Framework || + testFrameworks.size() > 1 && ContainerUtil.find(testFrameworks, f -> f instanceof JUnit6Framework) != null) { + return JUnitStarter.JUNIT6_PARAMETER; + } if (testFramework instanceof JUnit5Framework || testFrameworks.size() > 1 && ContainerUtil.find(testFrameworks, f -> f instanceof JUnit5Framework) != null) { return JUnitStarter.JUNIT5_PARAMETER; @@ -760,28 +807,29 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState { if (DumbService.isDumb(project)) { return null; } - return findCustomJunit5TestEngineUsingPsi(globalSearchScope, project, psiFacade); + return hasCustomJupiterTestEngineUsingPsi(globalSearchScope, project, psiFacade, jupiterClassName); }).executeSynchronously(); - if (isCustomJUnit5UsingPsi != null) { - return isCustomJUnit5UsingPsi; + if (isCustomJUnitUsingPsi != null) { + return isCustomJUnitUsingPsi; } - return findCustomJUnit5TestEngineUsingClassLoader(globalSearchScope, psiFacade); + return findCustomJupiterTestEngineUsingClassLoader(globalSearchScope, psiFacade); } - private boolean findCustomJUnit5TestEngineUsingClassLoader(@NotNull GlobalSearchScope globalSearchScope, - @NotNull JavaPsiFacade psiFacade) { + private boolean findCustomJupiterTestEngineUsingClassLoader(@NotNull GlobalSearchScope globalSearchScope, + @NotNull JavaPsiFacade psiFacade) { boolean hasPlatformEngine = ReadAction.compute(() -> { PsiPackage aPackage = psiFacade.findPackage(JUnitCommonClassNames.ORG_JUNIT_PLATFORM_ENGINE); return aPackage != null && aPackage.getDirectories(globalSearchScope).length > 0; @@ -793,7 +841,7 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState files = FilenameIndex.getVirtualFilesByName(PsiJavaModule.MODULE_INFO_FILE, globalSearchScope); if (!files.isEmpty() && ReferencesSearch.search(testEngine, GlobalSearchScope.filesScope(project, files)).anyMatch(ref -> isCustomEngineProvided(testEngine, ref))) { @@ -815,7 +864,7 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState psiManager.findFile(f)) .filter(Objects::nonNull) @@ -823,7 +872,7 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState r.resolve()) .filter(e -> e instanceof PsiClass) .map(e -> (PsiClass)e) - .filter(c -> isCustomJunit5TestEngineName(c.getQualifiedName())) + .filter(c -> isCustomJupiterTestEngineName(c.getQualifiedName())) .anyMatch(c -> InheritanceUtil.isInheritorOrSelf(c, testEngine, true)); } @@ -838,7 +887,7 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState> classNames) { @@ -167,7 +167,7 @@ public class TestPackage extends TestObject { collectClassesRecursively(classFilter, acceptClassCondition, classes); } else { - LinkedHashSet psiClasses = new LinkedHashSet<>(); + Set psiClasses = new LinkedHashSet<>(); ConfigurationUtil.findAllTestClasses(classFilter, module, psiClasses); psiClasses.stream().map(PsiLocation::fromPsiElement).forEach(classes::add); } @@ -198,7 +198,7 @@ public class TestPackage extends TestObject { final JUnitConfiguration.Data data = getConfiguration().getPersistentData(); final Project project = getConfiguration().getProject(); final SourceScope sourceScope = data.getScope().getSourceScope(getConfiguration()); - if (sourceScope == null || !JUnitStarter.JUNIT5_PARAMETER.equals(getRunner())) { //check for junit 5 + if (sourceScope == null || !JUPITER_RUNNERS.contains(getRunner())) { //check for junit 5/6 ReadAction.run(() -> JUnitUtil.checkTestCase(sourceScope, project)); } createTempFiles(javaParameters); diff --git a/plugins/junit/src/com/intellij/execution/junit/UniqueIdConfigurationProducer.java b/plugins/junit/src/com/intellij/execution/junit/UniqueIdConfigurationProducer.java index d69c57f9a274..4b8e193650f3 100644 --- a/plugins/junit/src/com/intellij/execution/junit/UniqueIdConfigurationProducer.java +++ b/plugins/junit/src/com/intellij/execution/junit/UniqueIdConfigurationProducer.java @@ -94,8 +94,8 @@ public final class UniqueIdConfigurationProducer extends JUnitConfigurationProdu Project project = context.getProject(); GlobalSearchScope searchScope = module != null ? GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module) : GlobalSearchScope.projectScope(project); - if (!DumbModeAccessType.RELIABLE_DATA_ONLY.ignoreDumbMode(() -> JUnitUtil.isJUnit5(searchScope, project) || - TestObject.hasJUnit5EnginesAPI(searchScope, JavaPsiFacade.getInstance(project)))) { + if (!DumbModeAccessType.RELIABLE_DATA_ONLY.ignoreDumbMode(() -> JUnitUtil.isJUnit5(searchScope, project) || + TestObject.hasJupiterEnginesAPI(searchScope, JavaPsiFacade.getInstance(project)))) { return null; } return diff --git a/plugins/junit/src/com/intellij/execution/junit/codeInspection/JUnit3StyleTestMethodInJUnit4ClassInspection.java b/plugins/junit/src/com/intellij/execution/junit/codeInspection/JUnit3StyleTestMethodInJUnit4ClassInspection.java index 64b376842323..775265f6914c 100644 --- a/plugins/junit/src/com/intellij/execution/junit/codeInspection/JUnit3StyleTestMethodInJUnit4ClassInspection.java +++ b/plugins/junit/src/com/intellij/execution/junit/codeInspection/JUnit3StyleTestMethodInJUnit4ClassInspection.java @@ -15,10 +15,13 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.List; + /** * @author Bas Leijdekkers */ public final class JUnit3StyleTestMethodInJUnit4ClassInspection extends BaseInspection { + private static final List IGNORED_FRAMEWORKS = List.of("JUnit4", "JUnit5", "JUnit6"); @Override protected @NotNull String buildErrorString(Object... infos) { @@ -49,7 +52,7 @@ public final class JUnit3StyleTestMethodInJUnit4ClassInspection extends BaseInsp if (testFramework != null) { if (testFramework.isTestMethod(method, false)) { final @NonNls String testFrameworkName = testFramework.getName(); - if (testFrameworkName.equals("JUnit4") || testFrameworkName.equals("JUnit5")) return; + if (IGNORED_FRAMEWORKS.contains(testFrameworkName)) return; } if (AnnotationUtil.isAnnotated(method, "org.junit.Ignore", 0) || testFramework.findSetUpMethod(containingClass) == method || diff --git a/plugins/junit/src/com/intellij/execution/junit/codeInspection/naming/JUnit4MethodNamingConvention.java b/plugins/junit/src/com/intellij/execution/junit/codeInspection/naming/JUnit4MethodNamingConvention.java index cc0c15a84780..fdd836216722 100644 --- a/plugins/junit/src/com/intellij/execution/junit/codeInspection/naming/JUnit4MethodNamingConvention.java +++ b/plugins/junit/src/com/intellij/execution/junit/codeInspection/naming/JUnit4MethodNamingConvention.java @@ -1,4 +1,4 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.execution.junit.codeInspection.naming; import com.intellij.codeInspection.naming.NamingConvention; @@ -20,7 +20,7 @@ public final class JUnit4MethodNamingConvention extends NamingConvention action.getText()) .filter(name -> name.startsWith("Add")).collect(Collectors.toSet()); assertAll("Detected frameworks: " + frameworks, - () -> assertTrue(frameworks.contains("Add 'JUnit5.8.1' to classpath"))); + () -> assertTrue(frameworks.contains("Add 'JUnit5' to classpath"))); myFixture.configureByText("MyTest.java", "class MyTest {@DisplayName void method() {}}"); @@ -179,7 +179,7 @@ public class JUnit5AcceptanceTest extends JUnit5CodeInsightTest { .map(action -> action.getText()) .filter(name -> name.startsWith("Add")).collect(Collectors.toSet()); assertAll("Detected frameworks: " + displayNameFrameworks, - () -> assertTrue(displayNameFrameworks.contains("Add 'JUnit5.8.1' to classpath"))); + () -> assertTrue(displayNameFrameworks.contains("Add 'JUnit5' to classpath"))); } @Test diff --git a/plugins/junit6_rt/BUILD.bazel b/plugins/junit6_rt/BUILD.bazel new file mode 100644 index 000000000000..8a27d7054f07 --- /dev/null +++ b/plugins/junit6_rt/BUILD.bazel @@ -0,0 +1,17 @@ +### auto-generated section `build intellij.junit.v6.rt` start +load("@rules_jvm//:jvm.bzl", "jvm_library") + +jvm_library( + name = "junit-v6-rt", + module_name = "intellij.junit.v6.rt", + visibility = ["//visibility:public"], + srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True), + kotlinc_opts = "@community//:k17", + deps = [ + "//plugins/junit_rt:junit-rt", + "//java/java-runtime:rt", + "@lib//:junit6Launcher-provided", + "@lib//:junit6-provided", + ] +) +### auto-generated section `build intellij.junit.v6.rt` end \ No newline at end of file diff --git a/plugins/junit6_rt/intellij.junit.v6.rt.iml b/plugins/junit6_rt/intellij.junit.v6.rt.iml new file mode 100644 index 000000000000..d25324530cdf --- /dev/null +++ b/plugins/junit6_rt/intellij.junit.v6.rt.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/junit6_rt/lib/opentest4j-1.3.0.jar b/plugins/junit6_rt/lib/opentest4j-1.3.0.jar new file mode 100644 index 000000000000..7ec7bc5494c6 Binary files /dev/null and b/plugins/junit6_rt/lib/opentest4j-1.3.0.jar differ diff --git a/plugins/junit6_rt/src/com/intellij/junit6/JUnit6IdeaTestRunner.java b/plugins/junit6_rt/src/com/intellij/junit6/JUnit6IdeaTestRunner.java new file mode 100644 index 000000000000..30efcc7ddb2f --- /dev/null +++ b/plugins/junit6_rt/src/com/intellij/junit6/JUnit6IdeaTestRunner.java @@ -0,0 +1,159 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.rt.execution.junit.IDEAJUnitListener; +import com.intellij.rt.execution.junit.IDEAJUnitListenerEx; +import com.intellij.rt.junit.IdeaTestRunner; +import org.jspecify.annotations.NonNull; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.EngineDescriptor; +import org.junit.platform.launcher.*; +import org.junit.platform.launcher.core.LauncherFactory; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public final class JUnit6IdeaTestRunner implements IdeaTestRunner { + private final List myExecutionListeners = new ArrayList<>(); + private ArrayList myListeners; + private Launcher myLauncher; + + public JUnit6IdeaTestRunner() { + Runnable warmup = (Runnable) Proxy.newProxyInstance( + JUnit6IdeaTestRunner.class.getClassLoader(), + new Class[]{Runnable.class}, + (proxy, method, args) -> null); + warmup.run(); + } + + @Override + public void createListeners(ArrayList listeners, int count) { + myListeners = listeners; + do { + JUnit6TestExecutionListener currentListener = new JUnit6TestExecutionListener(); + myExecutionListeners.add(currentListener); + if (count > 2) { + currentListener.initializeIdSuffix(count); + } + } + while (--count > 0); + myLauncher = LauncherFactory.create(); + } + + @Override + public int startRunnerWithArgs(String[] args, String programParam, int count, boolean sendTree) { + try { + JUnit6TestExecutionListener listener = myExecutionListeners.get(0); + listener.initializeIdSuffix(!sendTree); + final String[] packageNameRef = new String[1]; + final LauncherDiscoveryRequest discoveryRequest = JUnit6TestRunnerUtil.buildRequest(args, packageNameRef, programParam); + List listeners = new ArrayList<>(); + listeners.add(listener); + for (String listenerClassName : myListeners) { + final IDEAJUnitListener junitListener = (IDEAJUnitListener)Class.forName(listenerClassName).getDeclaredConstructor().newInstance(); + listeners.add(new MyCustomListenerWrapper(junitListener)); + } + if (sendTree) { + for (JUnit6TestExecutionListener executionListener : myExecutionListeners) { + executionListener.setRootName(packageNameRef[0]); + executionListener.setSendTree(); + } + } + + myLauncher.execute(discoveryRequest, listeners.toArray(new TestExecutionListener[0])); + + return listener.wasSuccessful() ? 0 : -1; + } + catch (Exception e) { + System.err.println("Internal Error occurred."); + e.printStackTrace(System.err); + return -2; + } + finally { + if (count > 0) myExecutionListeners.remove(0); + } + } + + private TestPlan myForkedTestPlan; + private static final TestIdentifier FAKE_ROOT = TestIdentifier.from(new EngineDescriptor(UniqueId.forEngine("FAKE_ENGINE"), "FAKE ENGINE")); + @Override + public TestIdentifier getTestToStart(String[] args, String name) { + final LauncherDiscoveryRequest discoveryRequest = JUnit6TestRunnerUtil.buildRequest(args, new String[1], ""); + myForkedTestPlan = LauncherFactory.create().discover(discoveryRequest); + final Set roots = myForkedTestPlan.getRoots(); + if (roots.isEmpty()) return null; + List nonEmptyRoots = roots.stream() + .filter(identifier -> !myForkedTestPlan.getChildren(identifier).isEmpty()) + .toList(); + if (nonEmptyRoots.isEmpty()) return null; + return nonEmptyRoots.size() == 1 ? nonEmptyRoots.get(0) : FAKE_ROOT; + } + + @Override + public List getChildTests(TestIdentifier description) { + if (description == FAKE_ROOT) { + return myForkedTestPlan.getRoots() + .stream() + .flatMap(root -> myForkedTestPlan.getChildren(root).stream()) + .collect(Collectors.toList()); + } + return new ArrayList<>(myForkedTestPlan.getChildren(description)); + } + + /** + * {@link com.intellij.execution.junit.TestClass#getForkMode()} + */ + @Override + public String getStartDescription(TestIdentifier child) { + if (myForkedTestPlan.getParent(child).isEmpty()) { + //if fork mode is "repeat", then the only child is the corresponding class + child = myForkedTestPlan.getChildren(child).iterator().next(); + } + final TestIdentifier testIdentifier = child; + final String className = JUnit6TestExecutionListener.getClassName(testIdentifier); + final String methodSignature = JUnit6TestExecutionListener.getMethodSignature(testIdentifier); + if (methodSignature != null) { + return className + "," + methodSignature; + } + return className != null ? className : testIdentifier.getDisplayName(); + } + + @Override + public String getTestClassName(TestIdentifier child) { + return child.toString(); + } + + private static class MyCustomListenerWrapper implements TestExecutionListener { + private final IDEAJUnitListener myJunitListener; + + MyCustomListenerWrapper(IDEAJUnitListener junitListener) { + myJunitListener = junitListener;} + + @Override + public void executionStarted(TestIdentifier testIdentifier) { + if (testIdentifier.isTest()) { + final String className = JUnit6TestExecutionListener.getClassName(testIdentifier); + final String methodName = JUnit6TestExecutionListener.getMethodName(testIdentifier); + myJunitListener.testStarted(className, methodName); + } + } + + @Override + public void executionFinished(TestIdentifier testIdentifier, @NonNull TestExecutionResult testExecutionResult) { + if (testIdentifier.isTest()) { + final String className = JUnit6TestExecutionListener.getClassName(testIdentifier); + final String methodName = JUnit6TestExecutionListener.getMethodName(testIdentifier); + if (myJunitListener instanceof IDEAJUnitListenerEx) { + ((IDEAJUnitListenerEx)myJunitListener).testFinished(className, methodName, testExecutionResult.getStatus() == TestExecutionResult.Status.SUCCESSFUL); + } + else { + myJunitListener.testFinished(className, methodName); + } + } + } + } +} diff --git a/plugins/junit6_rt/src/com/intellij/junit6/JUnit6TestExecutionListener.java b/plugins/junit6_rt/src/com/intellij/junit6/JUnit6TestExecutionListener.java new file mode 100644 index 000000000000..9656d8f094a1 --- /dev/null +++ b/plugins/junit6_rt/src/com/intellij/junit6/JUnit6TestExecutionListener.java @@ -0,0 +1,431 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.junit4.ExpectedPatterns; +import com.intellij.rt.execution.junit.ComparisonFailureData; +import com.intellij.rt.execution.junit.MapSerializerUtil; +import org.jspecify.annotations.NonNull; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.CompositeTestSource; +import org.junit.platform.engine.support.descriptor.FileSource; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; +import org.opentest4j.MultipleFailuresError; + +import java.io.File; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.*; + +import static com.intellij.rt.execution.TestListenerProtocol.CLASS_CONFIGURATION; + +@SuppressWarnings("UseOfSystemOutOrSystemErr") +public class JUnit6TestExecutionListener implements TestExecutionListener { + private static final String NO_LOCATION_HINT = ""; + private static final String NO_LOCATION_HINT_VALUE = ""; + + private final PrintStream myPrintStream; + private TestPlan myTestPlan; + private long myCurrentTestStart; + private int myFinishCount = 0; + private String myRootName; + private String myPresentableName; + private boolean mySuccessful = true; + private String myIdSuffix = ""; + private boolean mySendTree; + + public JUnit6TestExecutionListener() { + this(System.out); + } + + public JUnit6TestExecutionListener(PrintStream printStream) { + myPrintStream = printStream; + myPrintStream.println("##teamcity[enteredTheMatrix]"); + } + + public boolean wasSuccessful() { + return mySuccessful; + } + + public void initializeIdSuffix(boolean forked) { + if (forked && myIdSuffix.isEmpty()) { + myIdSuffix = String.valueOf(System.currentTimeMillis()); + } + } + + public void initializeIdSuffix(int i) { + myIdSuffix = i + "th"; + } + + @Override + public void reportingEntryPublished(@NonNull TestIdentifier testIdentifier, ReportEntry entry) { + StringBuilder builder = new StringBuilder(); + builder.append("timestamp = ").append(entry.getTimestamp()); + entry.getKeyValuePairs().forEach((key, value) -> builder.append(", ").append(key).append(" = ").append(value)); + builder.append("\n"); + myPrintStream.println("##teamcity[testStdOut" + idAndName(testIdentifier) + " out = '" + escapeName(builder.toString()) + "']"); + } + + @Override + public void testPlanExecutionStarted(@NonNull TestPlan testPlan) { + myTestPlan = testPlan; + if (mySendTree) { + for (TestIdentifier root : myTestPlan.getRoots()) { + assert root.isContainer(); + for (TestIdentifier testIdentifier : myTestPlan.getChildren(root)) { + String legacyReportingName = testIdentifier.getLegacyReportingName(); + if (legacyReportingName.equals(myRootName)) setPresentableName(testIdentifier.getDisplayName()); + sendTreeUnderRoot(testIdentifier, new HashSet<>()); + } + } + myPrintStream.println("##teamcity[treeEnded]"); + } + + if (myRootName != null) { + int lastPointIdx = myRootName.lastIndexOf('.'); + String name = myRootName; + String comment = null; + if (lastPointIdx >= 0) { + name = myRootName.substring(lastPointIdx + 1); + comment = myRootName.substring(0, lastPointIdx); + } + + String messageName = (myPresentableName == null || myPresentableName.isEmpty()) ? name : myPresentableName; + + myPrintStream.println("##teamcity[rootName name = '" + escapeName(messageName) + + (comment != null ? ("' comment = '" + escapeName(comment)) : "") + "'" + + " location = 'java:suite://" + escapeName(myRootName) + + "']"); + } + } + + private void sendTreeUnderRoot(TestIdentifier root, HashSet visited) { + String idAndName = idAndName(root); + if (root.isContainer()) { + boolean skipContainer = shouldSkipContainer(root); + if (!skipContainer) myPrintStream.println("##teamcity[suiteTreeStarted" + idAndName + " " + getLocationHint(root) + "]"); + for (TestIdentifier childIdentifier : myTestPlan.getChildren(root)) { + if (visited.add(childIdentifier)) { + sendTreeUnderRoot(childIdentifier, visited); + } + else { + System.err.println("Identifier '" + getId(childIdentifier) + "' is reused"); + } + } + if (!skipContainer) myPrintStream.println("##teamcity[suiteTreeEnded" + idAndName + "]"); + } + else if (root.isTest()) { + myPrintStream.println("##teamcity[suiteTreeNode " + idAndName + " " + getLocationHint(root) + "]"); + } + } + + @Override + public void executionSkipped(@NonNull TestIdentifier testIdentifier, @NonNull String reason) { + executionStarted (testIdentifier); + executionFinished(testIdentifier, TestExecutionResult.Status.ABORTED, null, reason); + } + + @Override + public void executionStarted(TestIdentifier testIdentifier) { + if (testIdentifier.isTest()) { + testStarted(testIdentifier); + myCurrentTestStart = System.nanoTime(); + } + else if (!shouldSkipContainer(testIdentifier)) { + myFinishCount = 0; + myPrintStream.println("##teamcity[testSuiteStarted" + idAndName(testIdentifier) + getLocationHint(testIdentifier) + "]"); + } + } + + @Override + public void executionFinished(@NonNull TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + TestExecutionResult.Status status = testExecutionResult.getStatus(); + Throwable throwableOptional = testExecutionResult.getThrowable().orElse(null); + executionFinished(testIdentifier, status, throwableOptional, null); + mySuccessful &= TestExecutionResult.Status.SUCCESSFUL == testExecutionResult.getStatus(); + } + + private void executionFinished(TestIdentifier testIdentifier, + TestExecutionResult.Status status, + Throwable throwableOptional, + String reason) { + if (testIdentifier.isTest()) { + long duration = getDuration(); + if (status == TestExecutionResult.Status.FAILED) { + testFailure(testIdentifier, MapSerializerUtil.TEST_FAILED, throwableOptional, duration, reason, true); + } + else if (status == TestExecutionResult.Status.ABORTED) { + testFailure(testIdentifier, MapSerializerUtil.TEST_IGNORED, throwableOptional, duration, reason, true); + } + testFinished(testIdentifier, duration); + myFinishCount++; + } + else if (!shouldSkipContainer(testIdentifier) || status == TestExecutionResult.Status.FAILED){ + String messageName = null; + if (status == TestExecutionResult.Status.FAILED) { + messageName = MapSerializerUtil.TEST_FAILED; + } + else if (status == TestExecutionResult.Status.ABORTED) { + messageName = MapSerializerUtil.TEST_IGNORED; + } + if (messageName != null) { + Set descendants = myTestPlan != null ? myTestPlan.getDescendants(testIdentifier) : Collections.emptySet(); + if (status == TestExecutionResult.Status.FAILED) { + String parentId = getParentId(testIdentifier); + String nameAndId = " name='" + CLASS_CONFIGURATION + + "' nodeId='" + escapeName(getId(testIdentifier)) + + "' parentNodeId='" + escapeName(parentId) + "' "; + myPrintStream.println("##teamcity[testStarted " + nameAndId + " ]"); + testFailure(CLASS_CONFIGURATION, getId(testIdentifier), parentId, messageName, throwableOptional, 0, reason, true); + myPrintStream.println("##teamcity[testFinished" + nameAndId + "]"); + } + else if (descendants.isEmpty()) { + testFailure(testIdentifier, MapSerializerUtil.TEST_IGNORED, throwableOptional, 0, reason, true); + } + + if (!descendants.isEmpty() && myFinishCount == 0) { + for (TestIdentifier childIdentifier : descendants) { + testStarted(childIdentifier); + Throwable throwable = status == TestExecutionResult.Status.ABORTED ? throwableOptional : null; + testFailure(childIdentifier, MapSerializerUtil.TEST_IGNORED, throwable, 0, reason, status == TestExecutionResult.Status.ABORTED); + testFinished(childIdentifier, 0); + } + myFinishCount = 0; + } + } + myPrintStream.println("##teamcity[testSuiteFinished" + idAndName(testIdentifier) + "]"); + } + } + + private boolean shouldSkipContainer(TestIdentifier testIdentifier) { + UniqueId id = UniqueId.parse(testIdentifier.getUniqueId()); + List segments = id.getSegments(); + if (segments.isEmpty()) return false; + UniqueId.Segment lastSegment = segments.get(segments.size() - 1); + return lastSegment.getType().equals("engine") || + myRootName != null && myRootName.equals(lastSegment.getValue()); + } + + protected long getDuration() { + return (System.nanoTime() - myCurrentTestStart) / 1_000_000; + } + + private void testStarted(TestIdentifier testIdentifier) { + myPrintStream.println("##teamcity[testStarted" + idAndName(testIdentifier) + " " + getLocationHint(testIdentifier) + "]"); + } + + private void testFinished(TestIdentifier testIdentifier, long duration) { + myPrintStream.println("##teamcity[testFinished" + idAndName(testIdentifier) + (duration > 0 ? " duration='" + duration + "'" : "") + "]"); + } + + private void testFailure(TestIdentifier testIdentifier, + String messageName, + Throwable ex, + long duration, + String reason, + boolean includeThrowable) { + testFailure(testIdentifier.getDisplayName(), getId(testIdentifier), getParentId(testIdentifier), messageName, ex, duration, reason, includeThrowable); + } + + private void testFailure(String methodName, + String id, + String parentId, + String messageName, + Throwable ex, + long duration, + String reason, + boolean includeThrowable) { + Map attrs = new LinkedHashMap<>(); + attrs.put("name", methodName); + attrs.put("id", id); + attrs.put("nodeId", id); + attrs.put("parentNodeId", parentId); + if (duration > 0) { + attrs.put("duration", Long.toString(duration)); + } + if (reason != null) { + attrs.put("message", reason); + } + try { + if (ex != null) { + ComparisonFailureData failureData = null; + if (ex instanceof MultipleFailuresError && ((MultipleFailuresError)ex).hasFailures()) { + for (Throwable assertionError : ((MultipleFailuresError)ex).getFailures()) { + testFailure(methodName, id, parentId, messageName, assertionError, duration, reason, true); + } + } + else { + try { + failureData = ExpectedPatterns.createExceptionNotification(ex); + } + catch (Throwable ignore) {} + } + + if (includeThrowable || failureData == null) { + ComparisonFailureData.registerSMAttributes(failureData, getTrace(ex), ex.getMessage(), attrs, ex, "Comparison Failure: ", "expected: <"); + } + else { + ComparisonFailureData.registerSMAttributes(failureData, "", ex.getMessage(), attrs, ex, "Comparison Failure: ", "expected: <"); + } + } + } + finally { + myPrintStream.println(MapSerializerUtil.asString(messageName, attrs)); + } + } + + protected String getTrace(Throwable ex) { + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + ex.printStackTrace(writer); + return stringWriter.toString(); + } + + public void setRootName(String rootName) { + myRootName = rootName; + } + + public void setPresentableName(String presentableName) { + myPresentableName = presentableName; + } + + public void setSendTree() { + mySendTree = true; + } + + private String getId(TestIdentifier identifier) { + return identifier.getUniqueId() + myIdSuffix; + } + + private String idAndName(TestIdentifier testIdentifier) { + return " id='" + escapeName(getId(testIdentifier)) + + "' name='" + escapeName(testIdentifier.getDisplayName()) + + "' nodeId='" + escapeName(getId(testIdentifier)) + + "' parentNodeId='" + escapeName(getParentId(testIdentifier)) + "'"; + } + + private String getParentId(TestIdentifier testIdentifier) { + Optional parent = myTestPlan.getParent(testIdentifier); + return parent + .map(identifier -> shouldSkipContainer(identifier) ? getParentId(identifier) : identifier.getUniqueId() + myIdSuffix) + .orElse("0"); + } + + + private String getLocationHint(TestIdentifier root) { + return getLocationHint(root, myTestPlan.getParent(root).orElse(null)); + } + + public static String getLocationHint(TestIdentifier root, TestIdentifier rootParent) { + return root.getSource() + .map(testSource -> getLocationHintValue(testSource, rootParent != null ? rootParent.getSource().orElse(null) : null)) + .filter(maybeLocationHintValue -> !NO_LOCATION_HINT_VALUE.equals(maybeLocationHintValue)) + .map(locationHintValue -> "locationHint='" + locationHintValue + "'" + getMetaInfo(root)) + .orElse(NO_LOCATION_HINT); + } + + private static String getMetaInfo(TestIdentifier root) { + return root.getSource() + .map(testSource -> { + if (testSource instanceof MethodSource) { + return " metainfo='" + ((MethodSource)testSource).getMethodParameterTypes() + "'"; + } + if (testSource instanceof ClassSource) { + return ((ClassSource)testSource).getPosition() + .map(position -> + " metainfo='" + // Convert JUnit's 1-based values to 0-based + + (position.getLine() - 1) + ":" + + (position.getColumn().orElse(1) - 1) + "'") + .orElse(NO_LOCATION_HINT); + } + return NO_LOCATION_HINT; + }) + .orElse(NO_LOCATION_HINT); + } + + public static String getLocationHintValue(TestSource testSource, TestSource parentSource) { + + if (testSource instanceof CompositeTestSource compositeTestSource) { + for (TestSource sourceFromComposite : compositeTestSource.getSources()) { + String locationHintValue = getLocationHintValue(sourceFromComposite, parentSource); + if (!NO_LOCATION_HINT_VALUE.equals(locationHintValue)) { + return locationHintValue; + } + } + return NO_LOCATION_HINT_VALUE; + } + + if (testSource instanceof FileSource fileSource) { + File file = fileSource.getFile(); + String line = fileSource.getPosition() + .map(position -> ":" + position.getLine()) + .orElse(""); + return "file://" + file.getAbsolutePath() + line; + } + + if (testSource instanceof MethodSource methodSource) { + return javaLocation(methodSource.getClassName(), methodSource.getMethodName(), true); + } + + if (testSource instanceof ClassSource) { + String className = ((ClassSource)testSource).getClassName(); + return javaLocation(className, null, false); + } + + if (parentSource != null) { + return getLocationHintValue(parentSource,null); + } + + return NO_LOCATION_HINT_VALUE; + } + + private static String javaLocation(String className, String maybeMethodName, boolean isTest) { + String type = isTest ? "test" : "suite"; + String methodName = maybeMethodName == null ? "" : "/" + maybeMethodName; + String location = escapeName(className + methodName); + return "java:" + type + "://" + location; + } + + private static String escapeName(String str) { + return MapSerializerUtil.escapeStr(str, MapSerializerUtil.STD_ESCAPER); + } + + public static String getClassName(TestIdentifier description) { + return description.getSource().map(source -> { + if (source instanceof MethodSource) { + return ((MethodSource)source).getClassName(); + } + if (source instanceof ClassSource) { + return ((ClassSource)source).getClassName(); + } + return null; + }).orElse(null); + } + + public static String getMethodName(TestIdentifier testIdentifier) { + return testIdentifier.getSource().map((source) -> { + if (source instanceof MethodSource) { + return ((MethodSource)source).getMethodName(); + } + return null; + }).orElse(null); + } + + static String getMethodSignature(TestIdentifier testIdentifier) { + return testIdentifier.getSource().map((source) -> { + if (source instanceof MethodSource) { + String parameterTypes = ((MethodSource)source).getMethodParameterTypes(); + return ((MethodSource)source).getMethodName() + (parameterTypes != null ? "(" + parameterTypes + ")" : ""); + } + return null; + }).orElse(null); + } +} diff --git a/plugins/junit6_rt/src/com/intellij/junit6/JUnit6TestRunnerUtil.java b/plugins/junit6_rt/src/com/intellij/junit6/JUnit6TestRunnerUtil.java new file mode 100644 index 000000000000..4b52ee02a2b8 --- /dev/null +++ b/plugins/junit6_rt/src/com/intellij/junit6/JUnit6TestRunnerUtil.java @@ -0,0 +1,293 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import org.jspecify.annotations.NonNull; +import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.FilterResult; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.discovery.*; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.PostDiscoveryFilter; +import org.junit.platform.launcher.TagFilter; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class JUnit6TestRunnerUtil { + private static final Pattern VALUE_SOURCE_PATTERN = Pattern.compile("valueSource\\s(\\d+)"); + private static Class NESTED_CLASS_SELECTOR_CLASS = null; + + public static LauncherDiscoveryRequest buildRequest(String[] suiteClassNames, String[] packageNameRef, String param) { + if (suiteClassNames.length == 0) { + return null; + } + + LauncherDiscoveryRequestBuilder builder = LauncherDiscoveryRequestBuilder.request(); + + + if (suiteClassNames.length == 1 && suiteClassNames[0].charAt(0) == '@') { + // all tests in the package specified + try (BufferedReader reader = new BufferedReader(new FileReader(suiteClassNames[0].substring(1), StandardCharsets.UTF_8))) { + final String packageName = reader.readLine(); + if (packageName == null) return null; + + String tags = reader.readLine(); + String filters = reader.readLine(); + String line; + + List selectors = new ArrayList<>(); + while ((line = reader.readLine()) != null) { + DiscoverySelector selector = createSelector(line, null); + if (selector != null) { + selectors.add(selector); + } + } + if (hasBrokenSelector(selectors)) { + builder.filters(createMethodFilter(new ArrayList<>(selectors))); + for (int i = 0; i < selectors.size(); i++) { + DiscoverySelector selector = selectors.get(i); + if (selector instanceof MethodSelector) { + selectors.set(i, createClassSelector(((MethodSelector)selector).getClassName())); + } + } + } + packageNameRef[0] = packageName.isEmpty() ? "" : packageName; + if (selectors.isEmpty()) { + builder.selectors(DiscoverySelectors.selectPackage(packageName)); + } + else { + builder.selectors(selectors); + if (!packageName.isEmpty()) { + builder.filters(PackageNameFilter.includePackageNames(packageName)); + } + } + if (filters != null && !filters.isEmpty()) { + String[] classNames = filters.split("\\|\\|"); + for (String className : classNames) { + if (!className.contains("*")) { + try { + Class.forName(className, false, JUnit6TestRunnerUtil.class.getClassLoader()); + } + catch (ClassNotFoundException e) { + System.err.println(MessageFormat.format(ResourceBundle.getBundle("messages.RuntimeBundle").getString("junit.class.not.found"), className)); + } + } + } + builder.filters(ClassNameFilter.includeClassNamePatterns(classNames)); + } + if (tags != null && !tags.isEmpty()) { + builder.filters(TagFilter.includeTags(tags.split(" "))); + } + return builder.filters(ClassNameFilter.excludeClassNamePatterns("com\\.intellij\\.rt.*", "com\\.intellij\\.junit3.*")).build(); + } + catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + } + else { + DiscoverySelector selector = createSelector(suiteClassNames[0], packageNameRef); + if (selector instanceof MethodSelector) { + DiscoverySelector classSelector = createClassSelector(((MethodSelector)selector).getClassName()); + DiscoverySelector methodSelector = isNestedClassSelector(classSelector) + ? DiscoverySelectors.selectMethod(((NestedClassSelector)classSelector).getNestedClassName(), + ((MethodSelector)selector).getMethodName()) + : selector; + builder.filters(createMethodFilter(Collections.singletonList(methodSelector))); + selector = classSelector; + } + if (selector instanceof MethodSelector && param != null) { + DiscoverySelector methodSelectIteration = createMethodSelectIteration(selector, param); + if (methodSelectIteration != null) { + return builder.selectors(methodSelectIteration).build(); + } + } + assert selector != null : "selector by class name is never null"; + return builder.selectors(selector).build(); + } + + return null; + } + + private static boolean isNestedClassSelector(DiscoverySelector selector) { + if (NESTED_CLASS_SELECTOR_CLASS == null) { + try { + NESTED_CLASS_SELECTOR_CLASS = Class.forName("org.junit.platform.engine.discovery.NestedClassSelector"); + } + catch (ClassNotFoundException e) { + return false; + } + } + return NESTED_CLASS_SELECTOR_CLASS.isInstance(selector); + } + + private static boolean loadMethodByReflection(MethodSelector selector) { + try { + Class aClass = Class.forName(selector.getClassName()); + return ReflectionSupport.findMethod(aClass, selector.getMethodName(), selector.getParameterTypes()).isPresent(); + } + catch (ClassNotFoundException e) { + return false; + } + } + + private static boolean hasBrokenSelector(List selectors) { + for (DiscoverySelector selector : selectors) { + if (selector instanceof MethodSelector && !loadMethodByReflection((MethodSelector)selector)) { + return true; + } + } + + return false; + } + + private static PostDiscoveryFilter createMethodFilter(List selectors) { + return new PostDiscoveryFilter() { + @Override + public @NonNull FilterResult apply(TestDescriptor descriptor) { + return FilterResult.includedIf(shouldRun(descriptor), + () -> descriptor.getDisplayName() + " matches", + () -> descriptor.getDisplayName() + " doesn't match"); + } + + private boolean shouldRun(TestDescriptor descriptor) { + TestSource source = descriptor.getSource().orElse(null); + if (source instanceof MethodSource) { + for (DiscoverySelector selector : selectors) { + if (selector instanceof MethodSelector && + ((MethodSelector)selector).getMethodName().equals(((MethodSource)source).getMethodName()) && + (((MethodSelector)selector).getClassName().equals(((MethodSource)source).getClassName()) || + inNestedClass((MethodSource)source, createClassSelector(((MethodSelector)selector).getClassName())))) { + return true; + } + } + for (DiscoverySelector selector : selectors) { + if (selector instanceof ClassSelector && ((ClassSelector)selector).getClassName().equals(((MethodSource)source).getClassName()) || + inNestedClass((MethodSource)source, selector)) { + return true; + } + } + return false; + } + + return true; + } + + private static boolean inNestedClass(MethodSource source, DiscoverySelector selector) { + return isNestedClassSelector(selector) && + ((NestedClassSelector)selector).getNestedClassName().equals(source.getClassName()); + } + }; + } + + /** + * Unique id is prepended with prefix: @see com.intellij.execution.junit.TestUniqueId#getUniqueIdPresentation() + * Method contains ',' + */ + private static DiscoverySelector createSelector(String line, String[] packageNameRef) { + if (line.startsWith("\u001B")) { + String uniqueId = line.substring("\u001B".length()); + return DiscoverySelectors.selectUniqueId(uniqueId); + } + else if (line.startsWith("\u002B")) { + String directory = line.substring("\u002B".length()); + List selectors = DiscoverySelectors.selectClasspathRoots(Collections.singleton(Paths.get(directory))); + if (selectors.isEmpty()) { + return null; + } else { + return selectors.iterator().next(); + } + } + else if (line.contains(",")) { + MethodSelector selector = DiscoverySelectors.selectMethod(line.replaceFirst(",", "#")); + if (packageNameRef != null) { + packageNameRef[0] = selector.getClassName(); + } + return selector; + } + else { + if (packageNameRef != null) { + packageNameRef[0] = line; + } + + return createClassSelector(line); + } + } + + private static DiscoverySelector createClassSelector(String line) { + int nestedClassIdx = line.lastIndexOf("$"); + if (nestedClassIdx > 0) { + AtomicReference nestedClassSelector = new AtomicReference<>(); + ReflectionSupport.tryToLoadClass(line).ifFailure(__ -> { + nestedClassSelector.set(getNestedSelector(line, nestedClassIdx)); + }); + if (nestedClassSelector.get() != null) return nestedClassSelector.get(); + } + return DiscoverySelectors.selectClass(line); + } + + private static DiscoverySelector createMethodSelectIteration(DiscoverySelector methodSelector, String param) { + Integer index = null; + if (param != null) { + Matcher matcher = VALUE_SOURCE_PATTERN.matcher(param); + if (matcher.find()) { + String group = matcher.group(1); + try { + index = Integer.parseInt(group); + } + catch (NumberFormatException ignored) { + } + } + } + if (index != null) { + try { + return DiscoverySelectors.selectIteration(methodSelector, index); + } + catch (NoSuchMethodError e) { + return null; + } + } + return null; + } + private static NestedClassSelector getNestedSelector(String line, int nestedClassIdx) { + String enclosingClass = line.substring(0, nestedClassIdx); + String nestedClassName = line.substring(nestedClassIdx + 1); + DiscoverySelector enclosingClassSelector = createClassSelector(enclosingClass); + Class klass = isNestedClassSelector(enclosingClassSelector) + ? ((NestedClassSelector)enclosingClassSelector).getNestedClass() + : ((ClassSelector)enclosingClassSelector).getJavaClass(); + Class superclass = klass.getSuperclass(); + while (superclass != null) { + for (Class nested : superclass.getDeclaredClasses()) { + if (nested.getSimpleName().equals(nestedClassName)) { + List> enclosingClasses; + if (isNestedClassSelector(enclosingClassSelector)) { + enclosingClasses = new ArrayList<>(((NestedClassSelector)enclosingClassSelector).getEnclosingClasses()); + enclosingClasses.add(klass); + } + else { + enclosingClasses = Collections.singletonList(klass); + } + return DiscoverySelectors.selectNestedClass(enclosingClasses, nested); + } + } + superclass = superclass.getSuperclass(); + } + return null; + } +} diff --git a/plugins/junit6_rt_tests/BUILD.bazel b/plugins/junit6_rt_tests/BUILD.bazel new file mode 100644 index 000000000000..6628c431fc5e --- /dev/null +++ b/plugins/junit6_rt_tests/BUILD.bazel @@ -0,0 +1,45 @@ +### auto-generated section `build intellij.junit.v6.rt.tests` start +load("@rules_jvm//:jvm.bzl", "jvm_library") + +jvm_library( + name = "junit-v6-rt-tests_test_lib", + module_name = "intellij.junit.v6.rt.tests", + visibility = ["//visibility:public"], + srcs = glob(["test/**/*.kt", "test/**/*.java", "test/**/*.form"], allow_empty = True), + deps = [ + "//java/testFramework", + "//java/execution/impl", + "//plugins/junit", + "//java/java-tests:tests_test_lib", + "//aether-dependency-resolver", + "//aether-dependency-resolver:aether-dependency-resolver_test_lib", + "//java/compiler/openapi:java-compiler", + "//platform/analysis-api:analysis", + "//platform/core-api:core", + "//platform/core-ui", + "//platform/editor-ui-api:editor-ui", + "//platform/execution-impl", + "//jps/model-api:model", + "//platform/lang-core", + "//platform/projectModel-api:projectModel", + "//platform/smRunner", + "//platform/smRunner:smRunner_test_lib", + "//platform/testFramework", + "//platform/testFramework:testFramework_test_lib", + "//java/java-runtime:rt", + "@lib//:stream_ex", + "//platform/platform-util-io:ide-util-io", + "//platform/testFramework/junit5", + "//platform/testFramework/junit5:junit5_test_lib", + ] +) +### auto-generated section `build intellij.junit.v6.rt.tests` end + +### auto-generated section `test intellij.junit.v6.rt.tests` start +load("@community//build:tests-options.bzl", "jps_test") + +jps_test( + name = "junit-v6-rt-tests_test", + runtime_deps = [":junit-v6-rt-tests_test_lib"] +) +### auto-generated section `test intellij.junit.v6.rt.tests` end \ No newline at end of file diff --git a/plugins/junit6_rt_tests/intellij.junit.v6.rt.tests.iml b/plugins/junit6_rt_tests/intellij.junit.v6.rt.tests.iml new file mode 100644 index 000000000000..10039cc678be --- /dev/null +++ b/plugins/junit6_rt_tests/intellij.junit.v6.rt.tests.iml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6AcceptanceTest.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6AcceptanceTest.java new file mode 100644 index 000000000000..4f75085eb771 --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6AcceptanceTest.java @@ -0,0 +1,188 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.codeInsight.TestFrameworks; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.configurations.JavaParameters; +import com.intellij.execution.junit.*; +import com.intellij.execution.junit2.info.MethodLocation; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import com.intellij.testIntegration.TestFramework; +import one.util.streamex.StreamEx; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public class JUnit6AcceptanceTest extends JUnit6CodeInsightTest { + + @Test + void testFactoryMethods() { + PsiClass aClass = myFixture.addClass( + "/** @noinspection ALL*/ class MyTest {@org.junit.jupiter.api.TestFactory java.util.List tests() {return null;}}"); + PsiMethod factoryMethod = aClass.getMethods()[0]; + assertNotNull(factoryMethod); + assertTrue(JUnitUtil.isTestAnnotated(factoryMethod)); + } + + @Test + void testDefaultMethodInInterface() { + PsiClass aClass = myFixture.addClass("interface MyTest {@org.junit.jupiter.api.Test default void method() {}}"); + assertTrue(JUnitUtil.isTestClass(aClass, false, false)); + } + + @Test + void recognizedInnerClassesWithTestMethods() { + PsiClass aClass = myFixture + .addClass("import org.junit.jupiter.api.*; /** @noinspection ALL*/ class MyTest {@Nested class NTest { @Test void method() {}}}"); + assertTrue(JUnitUtil.isTestClass(aClass, false, false)); + PsiClass innerClass = aClass.getInnerClasses()[0]; + assertTrue(JUnitUtil.isTestClass(innerClass)); + assertTrue(JUnitUtil.isTestMethod(MethodLocation.elementInClass(innerClass.getMethods()[0], innerClass))); + } + + @Test + void rejectStaticMethods() { + PsiClass aClass = + myFixture.addClass("import org.junit.jupiter.api.*; /** @noinspection ALL*/ class MyTest { @Test static void method() {}}"); + assertFalse(JUnitUtil.isTestClass(aClass, false, false)); + assertFalse(JUnitUtil.isTestMethod(MethodLocation.elementInClass(aClass.getMethods()[0], aClass))); + } + + @Test + void rejectPrivateMethods() { + PsiClass aClass = + myFixture.addClass("import org.junit.jupiter.api.*; /** @noinspection ALL*/ class MyTest { @Test private void method() {}}"); + assertFalse(JUnitUtil.isTestClass(aClass, false, false)); + assertFalse(JUnitUtil.isTestMethod(MethodLocation.elementInClass(aClass.getMethods()[0], aClass))); + } + + @Test + void testFrameworkDetection() { + PsiClass aClass = myFixture.addClass("/** @noinspection ALL*/ class MyTest {@org.junit.jupiter.api.Test void method() {}}"); + assertNotNull(aClass); + TestFramework framework = TestFrameworks.detectFramework(aClass); + assertNotNull(framework, "No test framework detected"); + assertInstanceOf(JUnit6Framework.class, framework, framework.getName()); + } + + @Test + void testFrameworkDetectionWithMixedJunit4Junit6() throws ExecutionException { + myFixture.addClass("package org.junit; public @interface Test {}"); + PsiClass aClass = myFixture.addClass("/** @noinspection ALL*/ public class MyTest {@org.junit.jupiter.api.Test void method() {} @org.junit.Test void method2(){} }"); + assertNotNull(aClass); + TestFramework framework = TestFrameworks.detectFramework(aClass); + assertInstanceOf(JUnit4Framework.class, framework, framework.getName()); + JUnitConfiguration configuration = new JUnitConfiguration("", myFixture.getProject()); + configuration.beClassConfiguration(aClass); + JavaParameters parameters = configuration.getTestObject().createJavaParameters4Tests(); + assertTrue(parameters.getProgramParametersList().hasParameter("-junit6")); + } + + @Test + void testClassWithDisabledCondition() throws ExecutionException { + myFixture.addClass("package org.junit.jupiter.api; public @interface Disabled {}"); + PsiClass aClass = myFixture.addClass( + """ + /** @noinspection ALL*/ + @org.junit.jupiter.api.Disabled + public class MyTest {@org.junit.jupiter.api.Test void method() {}}"""); + assertNotNull(aClass); + TestFramework framework = TestFrameworks.detectFramework(aClass); + assertInstanceOf(JUnit6Framework.class, framework, framework.getName()); + JUnitConfiguration configuration = new JUnitConfiguration("", myFixture.getProject()); + configuration.beClassConfiguration(aClass); + JavaParameters parameters = configuration.getTestObject().createJavaParameters4Tests(); + assertTrue(parameters.getVMParametersList().hasParameter("-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition")); + } + + @Test + void testMetaDisabledClass() throws ExecutionException { + myFixture.addClass(""" + package org.test.sample; + @org.junit.jupiter.api.Disabled + public @interface MetaDisabled {}"""); + PsiClass aClass = myFixture.addClass( + """ + /** @noinspection ALL*/ + @org.test.sample.MetaDisabled + public class MyTest {@org.junit.jupiter.api.Test void method() {}}"""); + assertNotNull(aClass); + TestFramework framework = TestFrameworks.detectFramework(aClass); + assertInstanceOf(JUnit6Framework.class, framework, framework.getName()); + JUnitConfiguration configuration = new JUnitConfiguration("", myFixture.getProject()); + configuration.beClassConfiguration(aClass); + JavaParameters parameters = configuration.getTestObject().createJavaParameters4Tests(); + assertTrue(parameters.getVMParametersList().hasParameter("-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition")); + } + + @Test + void testWithDisabledCondition() throws ExecutionException { + PsiClass aClass = myFixture.addClass( + """ + /** @noinspection ALL*/ + public class MyTest { + @org.junit.jupiter.api.Disabled + @org.junit.jupiter.api.Test void method() {}}"""); + assertNotNull(aClass); + TestFramework framework = TestFrameworks.detectFramework(aClass); + assertInstanceOf(JUnit6Framework.class, framework, framework.getName()); + JUnitConfiguration configuration = new JUnitConfiguration("", myFixture.getProject()); + @SuppressWarnings("OptionalGetWithoutIsPresent") PsiMethod method = Arrays.stream(aClass.getMethods()).findFirst().get(); + configuration.beMethodConfiguration(MethodLocation.elementInClass(method, aClass)); + JavaParameters parameters = configuration.getTestObject().createJavaParameters4Tests(); + assertTrue(parameters.getVMParametersList().hasParameter("-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition")); + } + + @Test + void methodPresentations() { + myFixture.addClass("package a; public class TestInfo {}"); + PsiClass aClass = myFixture.addClass("/** @noinspection ALL*/ class MyTest {" + + " @org.junit.jupiter.api.Test void method() {}" + + " @org.junit.jupiter.api.Test void method(a.TestInfo info) {}" + + " @org.junit.jupiter.api.Test void method(T info) {}" + + " @org.junit.Test void method1() {}" + + " @org.junit.Test void method1(a.TestInfo info) {}" + + "}"); + assertNotNull(aClass); + + Stream expectedData = Arrays.stream(new String[]{"method", "method(a.TestInfo)", "method(a.TestInfo)", "method1", "method1"}); + StreamEx.of(aClass.getMethods()) + .zipWith(expectedData) + .forEach(e -> assertEquals(e.getValue(), JUnitConfiguration.Data.getMethodPresentation(e.getKey()))); + } + + @Test + void metaAnnotations() { + myFixture.addClass(""" + package a; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + @Retention(RetentionPolicy.RUNTIME) + @org.junit.jupiter.api.Test + @interface MyTest {}"""); + PsiClass aClass = myFixture.addClass(""" + class ATest { + @a.MyTest + void foo() {} + } + """); + assertTrue(JUnitUtil.isTestClass(aClass, false, false)); + assertTrue(JUnitUtil.isTestMethod(MethodLocation.elementInClass(aClass.getMethods()[0], aClass))); + } + + @Test + void customEngineOnly() { + PsiClass customEngineTest = myFixture.addClass("import org.junit.platform.commons.annotation.Testable;" + + " /** @noinspection ALL*/ " + + "@Testable\n" + + "class MyTests{}"); + assertTrue(JUnitUtil.isTestClass(customEngineTest)); + + PsiClass customEngineAnnotationOnSuper = myFixture.addClass("class MyCustomClass extends MyTests{}"); + assertTrue(JUnitUtil.isTestClass(customEngineAnnotationOnSuper)); + } +} diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6CodeInsightTest.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6CodeInsightTest.java new file mode 100644 index 000000000000..558a81e6162c --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6CodeInsightTest.java @@ -0,0 +1,38 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.testFramework.IdeaTestUtil; +import com.intellij.testFramework.fixtures.*; +import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl; +import com.intellij.testFramework.junit5.RunInEdt; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +@RunInEdt(writeIntent = true) +public abstract class JUnit6CodeInsightTest { + protected JavaCodeInsightTestFixture myFixture; + + @BeforeEach + void setUp() throws Exception { + IdeaTestFixtureFactory factory = IdeaTestFixtureFactory.getFixtureFactory(); + TestFixtureBuilder fixtureBuilder = factory.createLightFixtureBuilder( + new DefaultLightProjectDescriptor(IdeaTestUtil::getMockJdk21), "JUnit6CodeInsightTest" + ); + final IdeaProjectTestFixture fixture = fixtureBuilder.getFixture(); + myFixture = JavaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(fixture, new LightTempDirTestFixtureImpl(true)); + myFixture.setUp(); + + //init junit 6 framework + myFixture.addClass("package org.junit.platform.commons.annotation; public @interface Testable {}"); + myFixture.addClass("package org.junit.jupiter.api; public interface MethodOrderer { class Default implements MethodOrderer {} }"); + myFixture.addClass("package org.junit.jupiter.api; @org.junit.platform.commons.annotation.Testable public @interface Test {}"); + myFixture.addClass("package org.junit.jupiter.api; public @interface Nested {}"); + myFixture.addClass("package org.junit.jupiter.api; @org.junit.platform.commons.annotation.Testable public @interface TestFactory {}"); + myFixture.addClass("package org.junit.jupiter.params.provider; public @interface MethodSource {}"); + } + + @AfterEach + void tearDown() throws Exception { + myFixture.tearDown(); + } +} diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6Constants.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6Constants.java new file mode 100644 index 000000000000..f846ede40967 --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6Constants.java @@ -0,0 +1,6 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +public interface JUnit6Constants { + String VERSION = "6.0.0-RC2"; +} diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6EventsTest.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6EventsTest.java new file mode 100644 index 000000000000..c2d21d9892d1 --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6EventsTest.java @@ -0,0 +1,165 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.execution.junit.JUnitConfiguration; +import com.intellij.java.execution.AbstractTestFrameworkCompilingIntegrationTest; +import com.intellij.openapi.roots.ModuleRootModificationUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.testFramework.PlatformTestUtil; +import jetbrains.buildServer.messages.serviceMessages.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager; +import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.intellij.junit6.ServiceMessageUtil.replaceAttributes; + +@SuppressWarnings("SSBasedInspection") +public class JUnit6EventsTest extends AbstractTestFrameworkCompilingIntegrationTest { + @Override + protected String getTestContentRoot() { + return VfsUtilCore.pathToUrl(PlatformTestUtil.getCommunityPath() + "/plugins/junit6_rt_tests/testData/integration/events"); + } + + @Override + protected void setupModule() throws Exception { + super.setupModule(); + ModuleRootModificationUtil.updateModel(myModule, model -> model.addContentEntry(getTestContentRoot()) + .addSourceFolder(getTestContentRoot() + "/test", true)); + final ArtifactRepositoryManager repoManager = getRepoManager(); + addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.jupiter", "junit-jupiter-api", JUnit6Constants.VERSION), + repoManager); + } + + public void testMultipleFailures() throws Exception { + ProcessOutput output = doStartTestsProcess(createRunMethodConfiguration("com.intellij.junit6.testData.MyTestClass", "test1")); + assertEmpty(output.err); + + List messages = output.messages; + + Map tests = getStartedTests(messages); + // Ensure our test method started with a proper location hint + TestStarted started = tests.values().stream() + .filter(t -> "java:test://com.intellij.junit6.testData.MyTestClass/test1".equals(t.getAttributes().get("locationHint"))) + .findFirst().orElse(null); + assertNotNull(started); + assertEquals("java:test://com.intellij.junit6.testData.MyTestClass/test1", started.getAttributes().get("locationHint")); + String nodeId = started.getAttributes().get("nodeId"); + + // There should be multiple failures reported for the same test + List failed = messages.stream() + .filter(m -> nodeId.equals(m.getAttributes().get("nodeId"))) + .filter(m -> m instanceof TestFailed).toList(); + assertSize(3, failed); + + Set errors = failed.stream().filter(m -> m.getAttributes().get("expected") != null) + .map(m -> "expected='" + m.getAttributes().get("expected") + "', actual='" + m.getAttributes().get("actual") + "'") + .collect(Collectors.toSet()); + assertEquals(Set.of( + "expected='expected1', actual='actual1'", + "expected='expected2', actual='actual2'" + ), errors); + + // StdOut message should include our published entries + Set outs = messages.stream() + .filter(m -> nodeId.equals(m.getAttributes().get("nodeId"))) + .filter(m -> m instanceof TestStdOut) + .map(m -> m.getAttributes().get("out")) + .map(s -> s.replaceAll("timestamp = [0-9\\-:.T]+", "timestamp = ##timestamp##")) + .collect(Collectors.toSet()); + assertEquals(Set.of("timestamp = ##timestamp##, key1 = value1, stdout = out1\n"), outs); + } + + public void testContainerFailure() throws Exception { + ProcessOutput output = doStartTestsProcess(createRunMethodConfiguration("com.intellij.junit6.testData.MyTestClass", "brokenStream")); + assertEmpty(output.err); + + // Expect a configuration failure + List test = output.messages.stream().filter(BaseTestMessage.class::isInstance) + .map(BaseTestMessage.class::cast) + .filter(m -> m.getTestName().equals("Class Configuration")) + .map(m -> replaceAttributes(m, Map.of("details", "##details##"))) + .toList(); + assertEquals(List.of( + "##teamcity[testStarted name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']", + "##teamcity[testFailed name='Class Configuration' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0' error='true' message='java.lang.IllegalStateException: broken' details='##details##']", + "##teamcity[testFinished name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']" + ), test); + } + + public void testContainerDisabled() throws Exception { + ProcessOutput output = doStartTestsProcess(createRunClassConfiguration("com.intellij.junit6.testData.MyTestClass")); + assertEmpty(output.err); + + List tests = output.messages.stream().filter(m -> m instanceof BaseTestMessage) + .map(m -> replaceAttributes(m, Map.of( + "timestamp", "##timestamp##", + "duration", "##duration##", + "message", "##message##", + "details", "##details##" + ))) + .map(s -> s.replaceAll("timestamp = [0-9\\-:.T]+", "timestamp = ##timestamp##")).toList(); + + assertEquals(List.of( + "##teamcity[testStarted id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' name='disabledTest()' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' parentNodeId='0' locationHint='java:test://com.intellij.junit6.testData.MyTestClass/disabledTest' metainfo='']", + "##teamcity[testIgnored name='disabledTest()' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' parentNodeId='0' message='##message##']", + "##teamcity[testFinished id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' name='disabledTest()' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' parentNodeId='0']", + "##teamcity[testStarted id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' locationHint='java:test://com.intellij.junit6.testData.MyTestClass/test1' metainfo='org.junit.jupiter.api.TestReporter']", + "##teamcity[testStdOut id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' out='timestamp = ##timestamp##, key1 = value1, stdout = out1|n']", + "##teamcity[testFailed name='test1(TestReporter)' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='##message##' expected='expected1' actual='actual1' details='##details##']", + "##teamcity[testFailed name='test1(TestReporter)' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='##message##' expected='expected2' actual='actual2' details='##details##']", + "##teamcity[testFailed name='test1(TestReporter)' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='##message##' details='##details##']", + "##teamcity[testFinished id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##']", + "##teamcity[testIgnored name='brokenStreamDisabled()' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStreamDisabled()|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStreamDisabled()|]' parentNodeId='0' message='##message##']", + "##teamcity[testStarted name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']", + "##teamcity[testFailed name='Class Configuration' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0' error='true' message='##message##' details='##details##']", + "##teamcity[testFinished name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']" + ), tests); + } + + public void testEscaping() throws Exception { + ProcessOutput output = doStartTestsProcess(createRunMethodConfiguration("com.intellij.junit6.testData.AnnotationsTestClass", "test1")); + assertEmpty(output.err); + + Map tests = getStartedTests(output.messages); + TestStarted started = tests.values().stream() + .filter(t -> t.getAttributes().get("locationHint").equals("java:test://com.intellij.junit6.testData.AnnotationsTestClass/test1")) + .findFirst().orElse(null); + assertNotNull(started); + // TeamCity service messages encode names internally; here we ensure the original display name is preserved + assertEquals("test's method", started.getAttributes().get("name")); + } + + @NotNull + private RunConfiguration createRunMethodConfiguration(final String className, final String methodName) { + PsiClass aClass = JavaPsiFacade.getInstance(myProject).findClass(className, GlobalSearchScope.projectScope(myProject)); + assertNotNull(aClass); + PsiMethod method = aClass.findMethodsByName(methodName, false)[0]; + RunConfiguration configuration = createConfiguration(method); + assertInstanceOf(configuration, JUnitConfiguration.class); + return configuration; + } + + @NotNull + private RunConfiguration createRunClassConfiguration(final String className) { + PsiClass aClass = JavaPsiFacade.getInstance(myProject).findClass(className, GlobalSearchScope.projectScope(myProject)); + assertNotNull(aClass); + RunConfiguration configuration = createConfiguration(aClass); + assertInstanceOf(configuration, JUnitConfiguration.class); + return configuration; + } + + private static Map getStartedTests(List messages) { + return messages.stream().filter(TestStarted.class::isInstance).map(TestStarted.class::cast) + .collect(Collectors.toMap(t -> t.getAttributes().get("id"), t -> t)); + } +} diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6GenerationTest.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6GenerationTest.java new file mode 100644 index 000000000000..8a75b80c5516 --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6GenerationTest.java @@ -0,0 +1,44 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.testIntegration.BaseGenerateTestSupportMethodAction; +import com.intellij.testIntegration.TestIntegrationUtils; +import org.junit.jupiter.api.Test; + +public class JUnit6GenerationTest extends JUnit6CodeInsightTest { + @Test + void testMethodInTopLevelClass() { + doTest("import org.junit.jupiter.api.Test; class MyTest { @Test void m2(){}}", + """ + import org.junit.jupiter.api.Test; class MyTest { + @Test + void name() { + \s + } + + @Test void m2(){}}"""); + } + + @Test + void testMethodInNestedClass() { + doTest("import org.junit.jupiter.api.Nested; class MyTest { @Nested class NTest { }}", + """ + import org.junit.jupiter.api.Nested; + import org.junit.jupiter.api.Test; + + class MyTest { @Nested class NTest { + @Test + void name() { + \s + } + }}"""); + } + + private void doTest(String text, String expected) { + myFixture.configureByText("MyTest.java", text); + + new BaseGenerateTestSupportMethodAction.MyHandler(TestIntegrationUtils.MethodKind.TEST) + .invoke(myFixture.getProject(), myFixture.getEditor(), myFixture.getFile()); + myFixture.checkResult(expected); + } +} diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6NamingTest.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6NamingTest.java new file mode 100644 index 000000000000..669dc8afa831 --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6NamingTest.java @@ -0,0 +1,52 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.execution.junit.JUnitConfiguration; +import com.intellij.java.execution.AbstractTestFrameworkCompilingIntegrationTest; +import com.intellij.openapi.roots.ModuleRootModificationUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.testFramework.PlatformTestUtil; +import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager; +import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor; +import org.junit.jupiter.api.Assertions; + +import java.util.Arrays; + +public class JUnit6NamingTest extends AbstractTestFrameworkCompilingIntegrationTest { + @Override + protected String getTestContentRoot() { + return VfsUtilCore.pathToUrl(PlatformTestUtil.getCommunityPath() + "/plugins/junit6_rt_tests/testData/integration/naming"); + } + + @Override + protected void setupModule() throws Exception { + super.setupModule(); + ModuleRootModificationUtil.updateModel(myModule, model -> model.addContentEntry(getTestContentRoot()) + .addSourceFolder(getTestContentRoot() + "/test", true)); + final ArtifactRepositoryManager repoManager = getRepoManager(); + addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.jupiter", "junit-jupiter-api", JUnit6Constants.VERSION), repoManager); + addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.platform", "junit-platform-suite-api", JUnit6Constants.VERSION), repoManager); + } + + public void testArrayParameters() { + PsiClass aClass = JavaPsiFacade.getInstance(myProject).findClass("MyTest", GlobalSearchScope.projectScope(myProject)); + assertNotNull(aClass); + PsiJavaFile file = (PsiJavaFile)aClass.getContainingFile(); + String[] methodPresentations = + Arrays.stream(file.getClasses()[0].getMethods()) + .map(method -> JUnitConfiguration.Data.getMethodPresentation(method)) + .toArray(String[]::new); + Assertions.assertArrayEquals(new String[]{"foo(int[])", + "foo(int)", + "foo([Ljava.lang.String;)", + "foo(java.lang.String)", + "foo([LMyTest$Foo;)", + "foo(MyTest$Foo)"}, + methodPresentations, + Arrays.toString(methodPresentations)); + } +} diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6NavigationTest.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6NavigationTest.java new file mode 100644 index 000000000000..07fb7e8f116a --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6NavigationTest.java @@ -0,0 +1,67 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.execution.junit.JUnitConfiguration; +import com.intellij.java.execution.AbstractTestFrameworkCompilingIntegrationTest; +import com.intellij.openapi.roots.ModuleRootModificationUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.testFramework.PlatformTestUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager; +import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor; +import org.junit.jupiter.api.DisplayName; + +import java.util.List; +import java.util.Map; + +import static com.intellij.junit6.ServiceMessageUtil.replaceAttributes; + +@DisplayName("junit 6 navigation features: location strings, etc") +public class JUnit6NavigationTest extends AbstractTestFrameworkCompilingIntegrationTest { + @Override + protected String getTestContentRoot() { + return VfsUtilCore.pathToUrl(PlatformTestUtil.getCommunityPath() + "/plugins/junit6_rt_tests/testData/integration/navigation"); + } + + @Override + protected void setupModule() throws Exception { + super.setupModule(); + ModuleRootModificationUtil.updateModel(myModule, model -> model.addContentEntry(getTestContentRoot()) + .addSourceFolder(getTestContentRoot() + "/test", true)); + final ArtifactRepositoryManager repoManager = getRepoManager(); + addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.jupiter", "junit-jupiter-api", JUnit6Constants.VERSION), repoManager); + } + + public void testNavigation() throws Exception { + ProcessOutput output = doStartTestsProcess(createRunClassConfiguration("org.example.impl.NavTest")); + assertEmpty(output.err); + + List messages = output.messages.stream().filter(m -> m.getAttributes().containsKey("locationHint")) + .map(m -> m.getAttributes().get("locationHint").startsWith("file://") + ? replaceAttributes(m, Map.of("locationHint", "file://##path##")) + : m.asString()) + .toList(); + + assertEquals(List.of( + "##teamcity[suiteTreeStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' name='fileSourceDynamicTests()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/fileSourceDynamicTests' metainfo='']", + "##teamcity[suiteTreeNode id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' name='methodNavigation()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/methodNavigation' metainfo='']", + "##teamcity[testSuiteStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' name='fileSourceDynamicTests()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/fileSourceDynamicTests' metainfo='']", + "##teamcity[testStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]/|[dynamic-test:#1|]' name='fileSource' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]/|[dynamic-test:#1|]' parentNodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' locationHint='file://##path##']", + "##teamcity[testStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' name='methodNavigation()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/methodNavigation' metainfo='']" + ), messages); + } + + @NotNull + private RunConfiguration createRunClassConfiguration(final String className) { + PsiClass aClass = JavaPsiFacade.getInstance(myProject).findClass(className, GlobalSearchScope.projectScope(myProject)); + assertNotNull(aClass); + + RunConfiguration configuration = createConfiguration(aClass); + assertInstanceOf(configuration, JUnitConfiguration.class); + return configuration; + } +} diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6OldSuiteApiIntegrationTest.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6OldSuiteApiIntegrationTest.java new file mode 100644 index 000000000000..614a5fc274a4 --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6OldSuiteApiIntegrationTest.java @@ -0,0 +1,74 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.execution.junit.JUnitConfiguration; +import com.intellij.java.execution.AbstractTestFrameworkCompilingIntegrationTest; +import com.intellij.openapi.roots.ModuleRootModificationUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.testFramework.PlatformTestUtil; +import jetbrains.buildServer.messages.serviceMessages.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager; +import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class JUnit6OldSuiteApiIntegrationTest extends AbstractTestFrameworkCompilingIntegrationTest { + @Override + protected String getTestContentRoot() { + return VfsUtilCore.pathToUrl(PlatformTestUtil.getCommunityPath() + "/plugins/junit6_rt_tests/testData/integration/oldSuiteApi"); + } + + @Override + protected void setupModule() throws Exception { + super.setupModule(); + ModuleRootModificationUtil.updateModel(myModule, model -> model.addContentEntry(getTestContentRoot()) + .addSourceFolder(getTestContentRoot() + "/test", true)); + final ArtifactRepositoryManager repoManager = getRepoManager(); + addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.jupiter", "junit-jupiter-api", JUnit6Constants.VERSION), repoManager); + addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.platform", "junit-platform-suite-api", JUnit6Constants.VERSION), + repoManager); + } + + public void testRunClass() throws Exception { + ProcessOutput processOutput = doStartTestsProcess(createRunClassConfiguration("org.example.impl.MyTest")); + assertEmpty(processOutput.out); + assertEmpty(processOutput.err); + + List messages = processOutput.messages; + + Map tests = getStartedTests(messages); + assertEquals(Set.of("java:test://org.example.impl.MyTest/test"), + tests.values().stream().map(t -> t.getAttributes().get("locationHint")).collect(Collectors.toSet())); + assertEquals(Set.of(), getTestIds(messages, TestFailed.class)); + assertEquals(tests.keySet(), getTestIds(messages, TestFinished.class)); + } + + @NotNull + private RunConfiguration createRunClassConfiguration(final String className) { + PsiClass aClass = JavaPsiFacade.getInstance(myProject).findClass(className, GlobalSearchScope.projectScope(myProject)); + assertNotNull(aClass); + + RunConfiguration configuration = createConfiguration(aClass); + assertInstanceOf(configuration, JUnitConfiguration.class); + return configuration; + } + + + private static Map getStartedTests(List messages) { + return messages.stream().filter(TestStarted.class::isInstance).map(TestStarted.class::cast) + .collect(Collectors.toMap(t -> t.getAttributes().get("id"), t -> t)); + } + + + private static Set getTestIds(List messages, Class clazz) { + return messages.stream().filter(clazz::isInstance).map(clazz::cast).map(o -> o.getAttributes().get("id")).collect(Collectors.toSet()); + } +} \ No newline at end of file diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6SuiteApiIntegrationTest.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6SuiteApiIntegrationTest.java new file mode 100644 index 000000000000..addc1087c7d9 --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/JUnit6SuiteApiIntegrationTest.java @@ -0,0 +1,139 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.execution.junit.JUnitConfiguration; +import com.intellij.java.execution.AbstractTestFrameworkCompilingIntegrationTest; +import com.intellij.openapi.roots.ModuleRootModificationUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiPackage; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.testFramework.PlatformTestUtil; +import jetbrains.buildServer.messages.serviceMessages.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager; +import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class JUnit6SuiteApiIntegrationTest extends AbstractTestFrameworkCompilingIntegrationTest { + @Override + protected String getTestContentRoot() { + return VfsUtilCore.pathToUrl(PlatformTestUtil.getCommunityPath() + "/plugins/junit6_rt_tests/testData/integration/suiteApi"); + } + + @Override + protected void setupModule() throws Exception { + super.setupModule(); + ModuleRootModificationUtil.updateModel(myModule, model -> model.addContentEntry(getTestContentRoot()) + .addSourceFolder(getTestContentRoot() + "/test", true)); + final ArtifactRepositoryManager repoManager = getRepoManager(); + addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.jupiter", "junit-jupiter-api", JUnit6Constants.VERSION), repoManager); + addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.platform", "junit-platform-suite-api", JUnit6Constants.VERSION), repoManager); + } + + public void testRunClass() throws Exception { + ProcessOutput processOutput = doStartTestsProcess(createRunClassConfiguration("org.example.api.SmokeSuite")); + assertEmpty(processOutput.out); + assertEmpty(processOutput.err); + + List messages = processOutput.messages; + + Map tests = getStartedTests(messages); + assertEquals(Set.of("java:test://org.example.impl.MyTest/test"), + tests.values().stream().map(t -> t.getAttributes().get("locationHint")).collect(Collectors.toSet())); + assertEquals(Set.of(), getTestIds(messages, TestFailed.class)); + assertEquals(tests.keySet(), getTestIds(messages, TestFinished.class)); + } + + public void testRunPackage() throws Exception { + ProcessOutput processOutput = doStartTestsProcess(createRunPackageConfiguration("org.example.api")); + assertEmpty(processOutput.out); + assertEmpty(processOutput.err); + + List messages = processOutput.messages; + + Map tests = getStartedTests(messages); + assertEquals(Set.of("java:test://org.example.impl.MyTest/test", + "java:test://org.example.impl.FirstTest/test1", + "java:test://org.example.impl.FirstTest/test2", + "java:test://org.example.impl.SecondTest/test1", + "java:test://org.example.impl.SecondTest/test2"), + tests.values().stream().map(t -> t.getAttributes().get("locationHint")).collect(Collectors.toSet())); + assertEquals(getTestIds(tests, Set.of("java:test://org.example.impl.FirstTest/test1", + "java:test://org.example.impl.SecondTest/test2")), + getTestIds(messages, TestFailed.class)); + assertEquals(tests.keySet(), getTestIds(messages, TestFinished.class)); + } + + @NotNull + private RunConfiguration createRunClassConfiguration(final String className) { + PsiClass aClass = JavaPsiFacade.getInstance(myProject).findClass(className, GlobalSearchScope.projectScope(myProject)); + assertNotNull(aClass); + + RunConfiguration configuration = createConfiguration(aClass); + assertInstanceOf(configuration, JUnitConfiguration.class); + return configuration; + } + + @NotNull + private RunConfiguration createRunPackageConfiguration(final String packageName) { + PsiPackage aPackage = JavaPsiFacade.getInstance(myProject).findPackage(packageName); + assertNotNull(aPackage); + + RunConfiguration configuration = createConfiguration(aPackage); + assertInstanceOf(configuration, JUnitConfiguration.class); + return configuration; + } + + private static Map getStartedTests(List messages) { + return messages.stream().filter(TestStarted.class::isInstance).map(TestStarted.class::cast) + .collect(Collectors.toMap(t -> t.getAttributes().get("id"), t -> t)); + } + + private static Set getTestIds(Map tests, Set locationHints) { + return tests.entrySet().stream() + .filter(e -> locationHints.contains(e.getValue().getAttributes().get("locationHint"))) + .map(t -> t.getKey()) + .collect(Collectors.toSet()); + } + + private static Set getTestIds(List messages, Class clazz) { + return messages.stream().filter(clazz::isInstance).map(clazz::cast).map(o -> o.getAttributes().get("id")).collect(Collectors.toSet()); + } + + public void testRerunFailedFromSuite() throws Exception { + ProcessOutput initialOutput = doStartTestsProcess(createRunPackageConfiguration("org.example.api")); + assertEmpty(initialOutput.out); + assertEmpty(initialOutput.err); + + List initialMessages = initialOutput.messages; + Map started = getStartedTests(initialMessages); + Set failedIds = getTestIds(initialMessages, TestFailed.class); + + Set failedHints = failedIds.stream() + .map(id -> started.get(id)) + .map(t -> t.getAttributes().get("locationHint")) + .collect(Collectors.toSet()); + + assertEquals(Set.of( + "java:test://org.example.impl.FirstTest/test1", + "java:test://org.example.impl.SecondTest/test2" + ), failedHints); + + ProcessOutput rerunOutput = doStartTestsProcess(createRunPackageConfiguration("org.example.api"), failedHints); + assertEmpty(rerunOutput.out); + assertEmpty(rerunOutput.err); + + Map rerunStarted = getStartedTests(rerunOutput.messages); + Set rerunHints = rerunStarted.values().stream().map(t -> t.getAttributes().get("locationHint")).collect(Collectors.toSet()); + + assertEquals(failedHints, rerunHints); + assertEquals(rerunStarted.keySet(), getTestIds(rerunOutput.messages, TestFinished.class)); + } +} \ No newline at end of file diff --git a/plugins/junit6_rt_tests/test/com/intellij/junit6/ServiceMessageUtil.java b/plugins/junit6_rt_tests/test/com/intellij/junit6/ServiceMessageUtil.java new file mode 100644 index 000000000000..f8fe0456437b --- /dev/null +++ b/plugins/junit6_rt_tests/test/com/intellij/junit6/ServiceMessageUtil.java @@ -0,0 +1,20 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6; + +import jetbrains.buildServer.messages.serviceMessages.ServiceMessage; +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class ServiceMessageUtil { + public static @NotNull String replaceAttributes(@NotNull ServiceMessage message, @NotNull Map attributes) { + Map attrs = new LinkedHashMap<>(message.getAttributes()); + for (Map.Entry entry : attributes.entrySet()) { + if (attrs.containsKey(entry.getKey())) { + attrs.put(entry.getKey(), entry.getValue()); + } + } + return ServiceMessage.asString(message.getMessageName(), attrs); + } +} diff --git a/plugins/junit6_rt_tests/testData/integration/events/test/com/intellij/junit6/testData/AnnotationsTestClass.java b/plugins/junit6_rt_tests/testData/integration/events/test/com/intellij/junit6/testData/AnnotationsTestClass.java new file mode 100644 index 000000000000..3977bbc62e20 --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/events/test/com/intellij/junit6/testData/AnnotationsTestClass.java @@ -0,0 +1,11 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6.testData; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class AnnotationsTestClass { + @Test + @DisplayName("test's method") + void test1() {} +} diff --git a/plugins/junit6_rt_tests/testData/integration/events/test/com/intellij/junit6/testData/MyTestClass.java b/plugins/junit6_rt_tests/testData/integration/events/test/com/intellij/junit6/testData/MyTestClass.java new file mode 100644 index 000000000000..08680cfaa21c --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/events/test/com/intellij/junit6/testData/MyTestClass.java @@ -0,0 +1,45 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.junit6.testData; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.DynamicTest; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +public class MyTestClass { + @Test + void test1(TestReporter reporter) { + Map map = new LinkedHashMap(); + map.put("key1", "value1"); + map.put("stdout", "out1"); + reporter.publishEntry(map); + Assertions.assertAll("2 errors", + () -> Assertions.assertEquals("expected1", "actual1", "message1"), + () -> Assertions.assertEquals("expected2", "actual2", "message2") + ); + } + + @TestFactory + Stream brokenStream() { + throw new IllegalStateException("broken"); + } + + @org.junit.jupiter.api.Disabled("container disabled") + @TestFactory + Stream brokenStreamDisabled() { + return Stream.of(); + } + + @org.junit.jupiter.api.Disabled("disabled") + @Test + void disabledTest() {} +} diff --git a/plugins/junit6_rt_tests/testData/integration/naming/test/MyTest.java b/plugins/junit6_rt_tests/testData/integration/naming/test/MyTest.java new file mode 100644 index 000000000000..8fea8f023479 --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/naming/test/MyTest.java @@ -0,0 +1,13 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +import org.junit.jupiter.api.Test; + +class MyTest { + @Test void foo(int[] i) {} + @Test void foo(int i) {} + @Test void foo(String[] i) {} + @Test void foo(String i) {} + @Test void foo(Foo[] i) {} + @Test void foo(Foo i) {} + + static class Foo {} +} diff --git a/plugins/junit6_rt_tests/testData/integration/navigation/test/org/example/impl/NavTest.java b/plugins/junit6_rt_tests/testData/integration/navigation/test/org/example/impl/NavTest.java new file mode 100644 index 000000000000..9513f8587c2c --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/navigation/test/org/example/impl/NavTest.java @@ -0,0 +1,32 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.example.impl; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +public class NavTest { + @Test + void methodNavigation() {} + + @TestFactory + Collection fileSourceDynamicTests() { + List list = new ArrayList<>(); + try { + // Create a temporary file to force FileSource in JUnit Platform + URI uri = Files.createTempFile("navtest", ".txt").toUri(); + list.add(dynamicTest("fileSource", uri, () -> {})); + } catch (Exception ignored) { + } + return list; + } +} diff --git a/plugins/junit6_rt_tests/testData/integration/oldSuiteApi/test/org/example/impl/MyTest.java b/plugins/junit6_rt_tests/testData/integration/oldSuiteApi/test/org/example/impl/MyTest.java new file mode 100644 index 000000000000..03f0f0a68c6e --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/oldSuiteApi/test/org/example/impl/MyTest.java @@ -0,0 +1,10 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.example.impl; + +import org.junit.jupiter.api.Test; + +public class MyTest { + @Test + public void test() { + } +} \ No newline at end of file diff --git a/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/api/AllTests.java b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/api/AllTests.java new file mode 100644 index 000000000000..e91bf7fdd94a --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/api/AllTests.java @@ -0,0 +1,12 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.example.api; + +import org.example.impl.FirstTest; +import org.example.impl.SecondTest; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +@Suite +@SelectClasses({FirstTest.class, SecondTest.class}) +public class AllTests { +} \ No newline at end of file diff --git a/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/api/SmokeSuite.java b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/api/SmokeSuite.java new file mode 100644 index 000000000000..a127f4ee58ab --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/api/SmokeSuite.java @@ -0,0 +1,11 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.example.api; + +import org.example.impl.MyTest; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +@Suite +@SelectClasses({MyTest.class}) +public class SmokeSuite { +} \ No newline at end of file diff --git a/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/impl/FirstTest.java b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/impl/FirstTest.java new file mode 100644 index 000000000000..65c0182ce9db --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/impl/FirstTest.java @@ -0,0 +1,15 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.example.impl; + +import org.junit.jupiter.api.Test; + +public class FirstTest { + @Test + public void test1() { + throw new RuntimeException(); + } + + @Test + public void test2() { + } +} \ No newline at end of file diff --git a/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/impl/MyTest.java b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/impl/MyTest.java new file mode 100644 index 000000000000..03f0f0a68c6e --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/impl/MyTest.java @@ -0,0 +1,10 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.example.impl; + +import org.junit.jupiter.api.Test; + +public class MyTest { + @Test + public void test() { + } +} \ No newline at end of file diff --git a/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/impl/SecondTest.java b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/impl/SecondTest.java new file mode 100644 index 000000000000..fb1ec6a1b7cf --- /dev/null +++ b/plugins/junit6_rt_tests/testData/integration/suiteApi/test/org/example/impl/SecondTest.java @@ -0,0 +1,15 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.example.impl; + +import org.junit.jupiter.api.Test; + +public class SecondTest { + @Test + public void test1() { + } + + @Test + public void test2() { + throw new RuntimeException(); + } +} \ No newline at end of file diff --git a/plugins/junit_rt/src/com/intellij/rt/junit/JUnitStarter.java b/plugins/junit_rt/src/com/intellij/rt/junit/JUnitStarter.java index c02e6a6b8b58..9c8fde0cca45 100644 --- a/plugins/junit_rt/src/com/intellij/rt/junit/JUnitStarter.java +++ b/plugins/junit_rt/src/com/intellij/rt/junit/JUnitStarter.java @@ -7,6 +7,7 @@ import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -14,7 +15,6 @@ import java.util.List; /** * Before rename or move * - * @noinspection HardCodedStringLiteral * @see com.intellij.execution.junit.JUnitConfiguration#JUNIT_START_CLASS */ public final class JUnitStarter { @@ -24,12 +24,14 @@ public final class JUnitStarter { public static final String JUNIT3_PARAMETER = "-junit3"; public static final String JUNIT4_PARAMETER = "-junit4"; public static final String JUNIT5_PARAMETER = "-junit5"; + public static final String JUNIT6_PARAMETER = "-junit6"; private static final String JUNIT5_KEY = "idea.is.junit5"; private static final String SOCKET = "-socket"; private static final String JUNIT3_RUNNER_NAME = "com.intellij.junit3.JUnit3IdeaTestRunner"; private static final String JUNIT4_RUNNER_NAME = "com.intellij.junit4.JUnit4IdeaTestRunner"; private static final String JUNIT5_RUNNER_NAME = "com.intellij.junit5.JUnit5IdeaTestRunner"; + private static final String JUNIT6_RUNNER_NAME = "com.intellij.junit6.JUnit6IdeaTestRunner"; private static String ourForkMode; private static String ourCommandFileName; private static String ourWorkingDirs; @@ -44,13 +46,14 @@ public final class JUnitStarter { String agentName = processParameters(argList, listeners, name); - if (!JUNIT5_RUNNER_NAME.equals(agentName) && !canWorkWithJUnitVersion(System.err, agentName)) { + if (!Arrays.asList(JUNIT5_RUNNER_NAME, JUNIT6_RUNNER_NAME).contains(agentName) && !canWorkWithJUnitVersion(System.err, agentName)) { System.exit(-3); } if (!checkVersion(args, System.err)) { System.exit(-3); } + @SuppressWarnings("SSBasedInspection") String[] array = argList.toArray(new String[0]); int exitCode = prepareStreamsAndStart(array, agentName, listeners, name[0]); System.exit(exitCode); @@ -72,6 +75,9 @@ public final class JUnitStarter { else if (arg.equals(JUNIT5_PARAMETER)) { agentName = JUNIT5_RUNNER_NAME; } + else if (arg.equals(JUNIT6_PARAMETER)) { + agentName = JUNIT6_RUNNER_NAME; + } else { if (arg.startsWith("@name")) { params[0] = arg.substring("@name".length()); @@ -243,7 +249,7 @@ public final class JUnitStarter { public static void printClassesList(List classNames, String packageName, String category, String filters, File tempFile) throws IOException { - try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(tempFile), StandardCharsets.UTF_8))) { + try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(Files.newOutputStream(tempFile.toPath()), StandardCharsets.UTF_8))) { writer.println(packageName); //package name writer.println(category); //category writer.println(filters); //patterns diff --git a/plugins/kotlin/base/code-insight/src/org/jetbrains/kotlin/idea/testIntegration/framework/AbstractKotlinPsiBasedTestFramework.kt b/plugins/kotlin/base/code-insight/src/org/jetbrains/kotlin/idea/testIntegration/framework/AbstractKotlinPsiBasedTestFramework.kt index 2cf0b09b51ee..117ac19c98a3 100644 --- a/plugins/kotlin/base/code-insight/src/org/jetbrains/kotlin/idea/testIntegration/framework/AbstractKotlinPsiBasedTestFramework.kt +++ b/plugins/kotlin/base/code-insight/src/org/jetbrains/kotlin/idea/testIntegration/framework/AbstractKotlinPsiBasedTestFramework.kt @@ -20,12 +20,12 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.* abstract class AbstractKotlinPsiBasedTestFramework : KotlinPsiBasedTestFramework { - protected abstract val markerClassFqn: String + protected abstract val markerClassFqns: Collection protected abstract val disabledTestAnnotation: String protected abstract val allowTestMethodsInObject: Boolean protected open fun isFrameworkAvailable(element: KtElement): Boolean = - isFrameworkAvailable(element, markerClassFqn, true) + markerClassFqns.any { isFrameworkAvailable(element, it, true) } protected fun isFrameworkAvailable(element: KtElement, markerClassFqn: String, javaOnly: Boolean): Boolean { val module = element.module ?: return false diff --git a/plugins/kotlin/base/code-insight/src/org/jetbrains/kotlin/idea/testIntegration/framework/KotlinPsiBasedTestFramework.kt b/plugins/kotlin/base/code-insight/src/org/jetbrains/kotlin/idea/testIntegration/framework/KotlinPsiBasedTestFramework.kt index 38883400845e..742eb163b0d2 100644 --- a/plugins/kotlin/base/code-insight/src/org/jetbrains/kotlin/idea/testIntegration/framework/KotlinPsiBasedTestFramework.kt +++ b/plugins/kotlin/base/code-insight/src/org/jetbrains/kotlin/idea/testIntegration/framework/KotlinPsiBasedTestFramework.kt @@ -39,10 +39,10 @@ interface KotlinPsiBasedTestFramework { fun findTearDown(classOrObject: KtClassOrObject): KtNamedFunction? = null companion object { - const val KOTLIN_TEST_TEST = "kotlin.test.Test" - const val KOTLIN_TEST_IGNORE = "kotlin.test.Ignore" - const val KOTLIN_TEST_BEFORE_TEST = "kotlin.test.BeforeTest" - const val KOTLIN_TEST_AFTER_TEST = "kotlin.test.AfterTest" + const val KOTLIN_TEST_TEST: String = "kotlin.test.Test" + const val KOTLIN_TEST_IGNORE: String = "kotlin.test.Ignore" + const val KOTLIN_TEST_BEFORE_TEST: String = "kotlin.test.BeforeTest" + const val KOTLIN_TEST_AFTER_TEST: String = "kotlin.test.AfterTest" fun PsiElement?.asKtClassOrObject(): KtClassOrObject? = when (this) { diff --git a/plugins/kotlin/run-configurations/junit/resources/kotlin.run-configurations.junit.xml b/plugins/kotlin/run-configurations/junit/resources/kotlin.run-configurations.junit.xml index e35e8957a921..23d161f45738 100644 --- a/plugins/kotlin/run-configurations/junit/resources/kotlin.run-configurations.junit.xml +++ b/plugins/kotlin/run-configurations/junit/resources/kotlin.run-configurations.junit.xml @@ -13,6 +13,8 @@ order="before junit4"/> + diff --git a/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit3Framework.kt b/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit3Framework.kt index 3e14917a6062..893a913e46f7 100644 --- a/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit3Framework.kt +++ b/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit3Framework.kt @@ -33,8 +33,8 @@ import org.jetbrains.kotlin.psi.psiUtil.isPrivate class KotlinJUnit3Framework: JUnit3Framework(), KotlinPsiBasedTestFramework { private val psiBasedDelegate = object : AbstractKotlinPsiBasedTestFramework() { - override val markerClassFqn: String - get() = JUnitUtil.TEST_CASE_CLASS + override val markerClassFqns: Collection + get() = listOf(JUnitUtil.TEST_CASE_CLASS) override val disabledTestAnnotation: String get() = throw UnsupportedOperationException("JUnit3 does not support Ignore methods") diff --git a/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit4Framework.kt b/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit4Framework.kt index 4ead85675834..ff496f3cf103 100644 --- a/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit4Framework.kt +++ b/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit4Framework.kt @@ -27,7 +27,7 @@ import org.jetbrains.kotlin.psi.psiUtil.isPrivate class KotlinJUnit4Framework: JUnit4Framework(), KotlinPsiBasedTestFramework { private val psiBasedDelegate = object : AbstractKotlinPsiBasedTestFramework() { - override val markerClassFqn: String = JUnitUtil.TEST_ANNOTATION + override val markerClassFqns: Collection = listOf(JUnitUtil.TEST_ANNOTATION) override val disabledTestAnnotation: String = "org.junit.Ignore" override val allowTestMethodsInObject: Boolean = false diff --git a/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit5Framework.kt b/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit5Framework.kt index c255429355e7..73a730acf298 100644 --- a/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit5Framework.kt +++ b/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit5Framework.kt @@ -28,8 +28,8 @@ import org.jetbrains.kotlin.psi.KtNamedFunction class KotlinJUnit5Framework: JUnit5Framework(), KotlinPsiBasedTestFramework { private val psiBasedDelegate = object : AbstractKotlinPsiBasedTestFramework() { - override val markerClassFqn: String = JUnitUtil.TEST5_ANNOTATION - override val disabledTestAnnotation: String = "org.junit.jupiter.api.Disabled" + override val markerClassFqns: Collection = markerClassFQNames + override val disabledTestAnnotation: String = JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_DISABLED override val allowTestMethodsInObject: Boolean = true override fun checkTestClass(declaration: KtClassOrObject): ThreeState { @@ -83,7 +83,7 @@ class KotlinJUnit5Framework: JUnit5Framework(), KotlinPsiBasedTestFramework { NO } else if (declaration is KtClass && declaration.isInner()) { if (isAnnotated(declaration, JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_NESTED)) YES else NO - } else if (declaration.isTopLevel() && isAnnotated(declaration, "org.junit.jupiter.api.extension.ExtendWith")) { + } else if (declaration.isTopLevel() && isAnnotated(declaration, JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_EXTENSION_EXTEND_WITH)) { YES } else if (findAnnotatedFunction(declaration, testableAnnotations) != null) { YES @@ -188,9 +188,9 @@ class KotlinJUnit5Framework: JUnit5Framework(), KotlinPsiBasedTestFramework { private val METHOD_ANNOTATION_FQN = setOf( JUnitUtil.TEST5_ANNOTATION, KotlinPsiBasedTestFramework.KOTLIN_TEST_TEST, - "org.junit.jupiter.params.ParameterizedTest", - "org.junit.jupiter.api.RepeatedTest", - "org.junit.jupiter.api.TestFactory", + JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_PARAMETERIZED_TEST, + JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_REPEATED_TEST, + JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_TEST_FACTORY, "org.junit.jupiter.api.TestTemplate", "org.junitpioneer.jupiter.RetryingTest" ) diff --git a/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit6Framework.kt b/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit6Framework.kt new file mode 100644 index 000000000000..f44cb2581317 --- /dev/null +++ b/plugins/kotlin/run-configurations/junit/src/org/jetbrains/kotlin/idea/junit/KotlinJUnit6Framework.kt @@ -0,0 +1,201 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.idea.junit + +import com.intellij.execution.junit.JUnit6Framework +import com.intellij.execution.junit.JUnitUtil +import com.intellij.ide.fileTemplates.FileTemplateDescriptor +import com.intellij.java.analysis.OuterModelsModificationTrackerManager +import com.intellij.lang.Language +import com.intellij.psi.PsiElement +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.parentOfType +import com.intellij.util.ThreeState +import com.intellij.util.ThreeState.* +import com.siyeh.ig.junit.JUnitCommonClassNames +import org.jetbrains.kotlin.asJava.elements.KtLightElement +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginModeProvider +import org.jetbrains.kotlin.idea.testIntegration.framework.AbstractKotlinPsiBasedTestFramework +import org.jetbrains.kotlin.idea.testIntegration.framework.KotlinPsiBasedTestFramework +import org.jetbrains.kotlin.idea.testIntegration.framework.KotlinPsiBasedTestFramework.Companion.asKtClassOrObject +import org.jetbrains.kotlin.idea.testIntegration.framework.KotlinPsiBasedTestFramework.Companion.asKtNamedFunction +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtNamedDeclaration +import org.jetbrains.kotlin.psi.KtNamedFunction + +class KotlinJUnit6Framework: JUnit6Framework(), KotlinPsiBasedTestFramework { + private val psiBasedDelegate = object : AbstractKotlinPsiBasedTestFramework() { + + override val markerClassFqns: Collection = markerClassFQNames + override val disabledTestAnnotation: String = JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_DISABLED + override val allowTestMethodsInObject: Boolean = true + + override fun checkTestClass(declaration: KtClassOrObject): ThreeState { + val checkState = super.checkTestClass(declaration) + if (checkState != UNSURE) return checkState + return CachedValuesManager.getCachedValue(declaration) { + CachedValueProvider.Result.create( + checkJUnit6TestClass(declaration), + OuterModelsModificationTrackerManager.getTracker(declaration.project) + ) + } + } + + fun isPotentialTestClass(element: PsiElement): Boolean { + if (element.language != KotlinLanguage.INSTANCE) return false + val psiElement = (element as? KtLightElement<*, *>)?.kotlinOrigin ?: element + val ktClassOrObject = psiElement.parentOfType(true) ?: return false + + return CachedValuesManager.getCachedValue(ktClassOrObject) { + CachedValueProvider.Result.create( + checkJUnit6PotentialTestClass(ktClassOrObject) != NO, + OuterModelsModificationTrackerManager.getTracker(ktClassOrObject.project) + ) + } + } + + override fun isTestMethod(declaration: KtNamedFunction): Boolean { + if (!super.isTestMethod(declaration)) return false + if (declaration.annotationEntries.isEmpty()) return false + return isJUnit6TestMethod(declaration) + } + + private fun checkJUnit6TestClass(declaration: KtClassOrObject): ThreeState = + if (!isFrameworkAvailable(declaration)) { + NO + } else { + checkIsJUnit6LikeTestClass(declaration, false) + } + + private fun checkJUnit6PotentialTestClass(declaration: KtClassOrObject): ThreeState = + if (!isFrameworkAvailable(declaration) && !isFrameworkAvailable(declaration, KotlinPsiBasedTestFramework.KOTLIN_TEST_TEST, false)) { + NO + } else { + checkIsJUnit6LikeTestClass(declaration, true) + } + + private fun checkIsJUnit6LikeTestClass(declaration: KtClassOrObject, isPotential: Boolean): ThreeState = + if (isPotential) { + if (isUnderTestSources(declaration)) UNSURE else NO + } else if (!isFrameworkAvailable(declaration)) { + NO + } else if (declaration is KtClass && declaration.isInner()) { + if (isAnnotated(declaration, JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_NESTED)) YES else NO + } else if (declaration.isTopLevel() && isAnnotated(declaration, JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_EXTENSION_EXTEND_WITH)) { + YES + } else if (findAnnotatedFunction(declaration, testableAnnotations) != null) { + YES + } else { + UNSURE + } + + private fun isJUnit6TestMethod(method: KtNamedFunction): Boolean { + return isAnnotated(method, METHOD_ANNOTATION_FQN) + } + + override fun findSetUp(classOrObject: KtClassOrObject): KtNamedFunction? = + findAnnotatedFunction(classOrObject.takeIf { isTestClass(it) }, setUpAnnotations) + + override fun findTearDown(classOrObject: KtClassOrObject): KtNamedFunction? = + findAnnotatedFunction(classOrObject.takeIf { isTestClass(it) }, tearDownAnnotations) + + } + + override fun responsibleFor(declaration: KtNamedDeclaration): Boolean = + psiBasedDelegate.responsibleFor(declaration) + + override fun checkTestClass(declaration: KtClassOrObject): ThreeState = psiBasedDelegate.checkTestClass(declaration) + + override fun isTestClass(clazz: PsiElement): Boolean = + when (val checkTestClass = checkTestClass(clazz)) { + UNSURE -> super.isTestClass(clazz) + else -> checkTestClass == YES + } + + override fun isPotentialTestClass(clazz: PsiElement): Boolean = + isTestClass(clazz) || psiBasedDelegate.isPotentialTestClass(clazz) + + override fun findSetUpMethod(clazz: PsiElement): PsiElement? = + when (checkTestClass(clazz)) { + UNSURE -> super.findSetUpMethod(clazz) + NO -> null + else -> clazz.asKtClassOrObject()?.let(psiBasedDelegate::findSetUp) + } + + override fun findTearDownMethod(clazz: PsiElement): PsiElement? = + when (checkTestClass(clazz)) { + UNSURE -> super.findTearDownMethod(clazz) + NO -> null + else -> clazz.asKtClassOrObject()?.let(psiBasedDelegate::findTearDown) + } + + override fun isIgnoredMethod(element: PsiElement?): Boolean = + when (checkTestClass(element)) { + UNSURE -> super.isIgnoredMethod(element) + NO -> false + else -> element.asKtNamedFunction()?.let(psiBasedDelegate::isIgnoredMethod) ?: false + } + + override fun isTestMethod(element: PsiElement?): Boolean = + when (checkTestClass(element)) { + UNSURE -> super.isTestMethod(element) + NO -> false + else -> element.asKtNamedFunction()?.let(psiBasedDelegate::isTestMethod) ?: false + } + + override fun getLanguage(): Language = KotlinLanguage.INSTANCE + + override fun isTestMethod(declaration: KtNamedFunction): Boolean = + psiBasedDelegate.isTestMethod(declaration) + + override fun isIgnoredMethod(declaration: KtNamedFunction): Boolean = + psiBasedDelegate.isIgnoredMethod(declaration) + + override fun getSetUpMethodFileTemplateDescriptor(): FileTemplateDescriptor? { + return if (KotlinPluginModeProvider.isK1Mode()) { + super.getSetUpMethodFileTemplateDescriptor() + } else { + FileTemplateDescriptor("Kotlin JUnit5 SetUp Function.kt") + } + } + + override fun getTearDownMethodFileTemplateDescriptor(): FileTemplateDescriptor? { + return if (KotlinPluginModeProvider.isK1Mode()) { + super.getTearDownMethodFileTemplateDescriptor() + } else { + FileTemplateDescriptor("Kotlin JUnit5 TearDown Function.kt") + } + } + + override fun getTestMethodFileTemplateDescriptor(): FileTemplateDescriptor { + return if (KotlinPluginModeProvider.isK1Mode()) { + super.getTestMethodFileTemplateDescriptor() + } else { + FileTemplateDescriptor("Kotlin JUnit5 Test Function.kt") + } + } + + override fun getTestClassFileTemplateDescriptor(): FileTemplateDescriptor? = + if (KotlinPluginModeProvider.isK1Mode()) { + super.getTestClassFileTemplateDescriptor() + } else { + FileTemplateDescriptor("Kotlin JUnit5 Test Class.kt") + } +} + +private val METHOD_ANNOTATION_FQN = setOf( + JUnitUtil.TEST5_ANNOTATION, + KotlinPsiBasedTestFramework.KOTLIN_TEST_TEST, + JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_PARAMETERIZED_TEST, + JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_REPEATED_TEST, + JUnitCommonClassNames.ORG_JUNIT_JUPITER_API_TEST_FACTORY, + "org.junit.jupiter.api.TestTemplate", + "org.junitpioneer.jupiter.RetryingTest" +) + +private val setUpAnnotations = setOf(JUnitUtil.BEFORE_EACH_ANNOTATION_NAME, KotlinPsiBasedTestFramework.KOTLIN_TEST_BEFORE_TEST) +private val tearDownAnnotations = setOf(JUnitUtil.AFTER_EACH_ANNOTATION_NAME, KotlinPsiBasedTestFramework.KOTLIN_TEST_AFTER_TEST) +private val testableAnnotations = METHOD_ANNOTATION_FQN + setUpAnnotations + tearDownAnnotations + diff --git a/plugins/kotlin/run-configurations/testng/src/org/jetbrains/kotlin/idea/testng/KotlinTestNGFramework.kt b/plugins/kotlin/run-configurations/testng/src/org/jetbrains/kotlin/idea/testng/KotlinTestNGFramework.kt index d9bd41ad2d46..c1b4559bca01 100644 --- a/plugins/kotlin/run-configurations/testng/src/org/jetbrains/kotlin/idea/testng/KotlinTestNGFramework.kt +++ b/plugins/kotlin/run-configurations/testng/src/org/jetbrains/kotlin/idea/testng/KotlinTestNGFramework.kt @@ -30,7 +30,7 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs class KotlinTestNGFramework: TestNGFramework(), KotlinPsiBasedTestFramework { private val psiBasedDelegate = object : AbstractKotlinPsiBasedTestFramework() { - override val markerClassFqn: String = TestNGUtil.TEST_ANNOTATION_FQN + override val markerClassFqns: Collection = listOf(TestNGUtil.TEST_ANNOTATION_FQN) override val disabledTestAnnotation: String get() = throw UnsupportedOperationException("TestNG does not support Ignore methods") override val allowTestMethodsInObject: Boolean = false