+ * Enables multiple indexing modes for each test: smart mode and different versions of dumb mode.
+ * Dumb mode indexing may be tuned with @NeedsIndex
annotation on a class or a specific test method.
+ *
+ * @see TestIndexingMode
+ * @see MultipleScalaVersionsRunner
+ * @see com.intellij.testFramework.NeedsIndex
+ * @see SCL-21849
+ * @see Make functionality available during indexing
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RunWithAllIndexingModes {
+}
diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithJdkVersions.java b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithJdkVersions.java
new file mode 100644
index 00000000..220cf02b
--- /dev/null
+++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithJdkVersions.java
@@ -0,0 +1,16 @@
+package org.jetbrains.plugins.scala.util.runners;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.CONSTRUCTOR)
+public @interface RunWithJdkVersions {
+
+ /** unsupported JDK for scala SDK are filtered out in
+ * {@link ScalaVersionAwareTestsCollector#collectTests()}*/
+ TestJdkVersion[] value() default {};
+ TestJdkVersion[] extra() default {};
+}
diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithJdkVersionsFilter.java b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithJdkVersionsFilter.java
new file mode 100644
index 00000000..b5556396
--- /dev/null
+++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithJdkVersionsFilter.java
@@ -0,0 +1,16 @@
+package org.jetbrains.plugins.scala.util.runners;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Used to filter out all generated cases based on RunWithScalaVersions
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.CONSTRUCTOR)
+public @interface RunWithJdkVersionsFilter {
+
+ TestJdkVersion[] value() default {};
+}
diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithScalaVersions.java b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithScalaVersions.java
new file mode 100644
index 00000000..959e8c20
--- /dev/null
+++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithScalaVersions.java
@@ -0,0 +1,14 @@
+package org.jetbrains.plugins.scala.util.runners;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.CONSTRUCTOR)
+public @interface RunWithScalaVersions {
+
+ TestScalaVersion[] value() default {};
+ TestScalaVersion[] extra() default {};
+}
diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithScalaVersionsFilter.java b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithScalaVersionsFilter.java
new file mode 100644
index 00000000..c8cda93b
--- /dev/null
+++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithScalaVersionsFilter.java
@@ -0,0 +1,16 @@
+package org.jetbrains.plugins.scala.util.runners;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Used to filter out all generated cases based on RunWithScalaVersions
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.CONSTRUCTOR)
+public @interface RunWithScalaVersionsFilter {
+
+ TestScalaVersion[] value() default {};
+}
diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/ScalaVersionAwareTestsCollector.scala b/src/test/scala/org/jetbrains/plugins/scala/util/runners/ScalaVersionAwareTestsCollector.scala
new file mode 100644
index 00000000..564b0ae2
--- /dev/null
+++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/ScalaVersionAwareTestsCollector.scala
@@ -0,0 +1,165 @@
+package org.jetbrains.plugins.scala.util.runners
+
+import com.intellij.testFramework.TestIndexingModeSupporter
+import junit.framework.{Test, TestCase, TestSuite}
+import org.jetbrains.plugins.scala.base.ScalaSdkOwner
+import org.jetbrains.plugins.scala.util.runners.MultipleScalaVersionsRunner.findAnnotation
+import org.junit.internal.MethodSorter
+
+import java.lang.reflect.{Method, Modifier}
+import scala.collection.mutable
+import scala.collection.mutable.ArrayBuffer
+
+class ScalaVersionAwareTestsCollector(klass: Class[_ <: TestCase],
+ classScalaVersion: Seq[TestScalaVersion],
+ classJdkVersion: Seq[TestJdkVersion]) {
+
+ def collectTests(): Seq[(TestCase, TestScalaVersion, TestJdkVersion, TestIndexingMode)] = {
+ val result = ArrayBuffer.empty[(Test, TestScalaVersion, TestJdkVersion, TestIndexingMode)]
+
+ val tests = testsFromTestCase(klass)
+ tests.foreach {
+ case (test: ScalaSdkOwner, _, scalaVersion, jdkVersion, indexingMode) =>
+ val scalaVersionProd = scalaVersion.toProductionVersion
+ val jdkVersionProd = jdkVersion.toProductionVersion
+
+ test.injectedScalaVersion = scalaVersionProd // !! should be set before calling test.skip
+ test.injectedJdkVersion = jdkVersionProd
+
+ test match {
+ case test: TestIndexingModeSupporter => test.setIndexingMode(indexingMode.mode)
+ case _ =>
+ }
+
+ if (!test.skip) {
+ result.append((test, scalaVersion, jdkVersion, indexingMode))
+ }
+ case (warningTest, _, scalaVersion, jdkVersion, handler) =>
+ result.append((warningTest, scalaVersion, jdkVersion, handler))
+ }
+
+ result.map(t => (t._1.asInstanceOf[TestCase], t._2, t._3, t._4)).toSeq
+ }
+
+ // warning test or collection of tests (each test method is multiplied by the amount of versions it is run with)
+ private def testsFromTestCase(klass: Class[_]): Seq[(Test, Method, TestScalaVersion, TestJdkVersion, TestIndexingMode)] = {
+ def warn(text: String) = Seq((warning(text), null, null, null, null))
+
+ try TestSuite.getTestConstructor(klass) catch {
+ case _: NoSuchMethodException =>
+ return warn(s"Class ${klass.getName} has no public constructor TestCase(String name) or TestCase()")
+ }
+
+ if (!Modifier.isPublic(klass.getModifiers))
+ return warn(s"Class ${klass.getName} is not public")
+
+ val withSuperClasses = Iterator.iterate[Class[_]](klass)(_.getSuperclass)
+ .takeWhile(_ != null)
+ .takeWhile(classOf[Test].isAssignableFrom)
+ .toArray
+
+ val visitedMethods = mutable.ArrayBuffer.empty[Method]
+ val tests = for {
+ superClass <- withSuperClasses
+ method <- MethodSorter.getDeclaredMethods(superClass)
+ if !isShadowed(method, visitedMethods)
+ (test, scalaVersion, jdkVersion, indexingMode) <- createTestMethods(klass, method)
+ } yield {
+ visitedMethods += method
+ (test, method, scalaVersion, jdkVersion, indexingMode)
+ }
+
+ if (tests.isEmpty) {
+ warn(s"No tests found in ${klass.getName}")
+ } else {
+ tests.toSeq
+ }
+ }
+
+ private def isShadowed(method: Method, results: Iterable[Method]): Boolean =
+ results.exists(isShadowed(method, _))
+
+ private def isShadowed(current: Method, previous: Method): Boolean =
+ previous.getName == current.getName &&
+ previous.getParameterTypes.toSeq == current.getParameterTypes.toSeq
+
+ private def createTestMethods(
+ theClass: Class[_],
+ method: Method
+ ): Seq[(Test, TestScalaVersion, TestJdkVersion, TestIndexingMode)] = {
+ val name = method.getName
+
+ if (isTestMethod(method)) {
+ val isPublic = isPublicMethod(method)
+
+ val effectiveScalaVersions = methodEffectiveScalaVersions(method, classScalaVersion)
+ val effectiveJdkVersions = methodEffectiveJdkVersions(method, classJdkVersion)
+ val effectiveIndexingModes = methodEffectiveIndexingModes(method)
+ for {
+ scalaVersion <- effectiveScalaVersions
+ jdkVersion <- effectiveJdkVersions
+ indexingMode <- effectiveIndexingModes
+ } yield {
+ val test = if (isPublic) {
+ TestSuite.createTest(theClass, name)
+ } else {
+ warning(s"Test method isn't public: ${method.getName}(${theClass.getCanonicalName})")
+ }
+ (test, scalaVersion, jdkVersion, indexingMode)
+ }
+ } else {
+ Seq()
+ }
+ }
+
+ private def methodEffectiveScalaVersions(method: Method, classVersions: Seq[TestScalaVersion]): Seq[TestScalaVersion] =
+ method.getAnnotation(classOf[RunWithScalaVersions]) match {
+ case null =>
+ classVersions
+ case annotation =>
+ val baseVersions = if (annotation.value.isEmpty) {
+ classVersions
+ } else {
+ annotation.value.toSeq
+ }
+ val extraVersions = annotation.extra.toSeq
+ (baseVersions ++ extraVersions).sorted.distinct
+ }
+
+ private def methodEffectiveJdkVersions(method: Method, classVersions: Seq[TestJdkVersion]): Seq[TestJdkVersion] =
+ method.getAnnotation(classOf[RunWithJdkVersions]) match {
+ case null =>
+ classVersions
+ case annotation =>
+ val baseVersions = if (annotation.value.isEmpty) {
+ classVersions
+ } else {
+ annotation.value.toSeq
+ }
+ val extraVersions = annotation.extra.toSeq
+ (baseVersions ++ extraVersions).sorted.distinct
+ }
+
+ // SCL-21849
+ private def methodEffectiveIndexingModes(method: Method): Seq[TestIndexingMode] =
+ if (!classOf[TestIndexingModeSupporter].isAssignableFrom(klass) || findAnnotation(klass, classOf[RunWithAllIndexingModes]).isEmpty) {
+ Seq(TestIndexingMode.SMART)
+ } else {
+ TestIndexingMode.values().toSeq.filterNot { mode =>
+ mode.shouldIgnore(klass) || mode.shouldIgnore(method)
+ }
+ }
+
+ private def isPublicMethod(m: Method): Boolean = Modifier.isPublic(m.getModifiers)
+
+ private def isTestMethod(m: Method): Boolean =
+ m.getParameterTypes.length == 0 &&
+ m.getName.startsWith("test") &&
+ m.getReturnType == Void.TYPE
+
+ // duplicate from `org.junit.framework.TestSuite.warning`
+ // the method is public in junit 4.12 in but private in `junit.jar` in IDEA jars which leads to a compilation error on TeamCity
+ private def warning(message: String) = new TestCase("warning") {
+ override protected def runTest(): Unit = org.junit.Assert.fail(message)
+ }
+}
diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/TestIndexingMode.java b/src/test/scala/org/jetbrains/plugins/scala/util/runners/TestIndexingMode.java
new file mode 100644
index 00000000..0632c731
--- /dev/null
+++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/TestIndexingMode.java
@@ -0,0 +1,35 @@
+package org.jetbrains.plugins.scala.util.runners;
+
+import com.intellij.testFramework.TestIndexingModeSupporter;
+import com.intellij.testFramework.TestIndexingModeSupporter.IndexingMode;
+import com.intellij.testFramework.TestIndexingModeSupporter.IndexingModeTestHandler;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.AnnotatedElement;
+
+// SCL-21849
+enum TestIndexingMode {
+ SMART("Smart mode", IndexingMode.SMART),
+ FULL(new TestIndexingModeSupporter.FullIndexSuite()),
+ RUNTIME(new TestIndexingModeSupporter.RuntimeOnlyIndexSuite()),
+ EMPTY(new TestIndexingModeSupporter.EmptyIndexSuite());
+
+ @Nullable
+ private IndexingModeTestHandler handler;
+ public final String label;
+ public final IndexingMode mode;
+
+ public boolean shouldIgnore(AnnotatedElement element) {
+ return handler != null && handler.shouldIgnore(element);
+ }
+
+ TestIndexingMode(IndexingModeTestHandler handler) {
+ this(handler.myTestNamePrefix, handler.getIndexingMode());
+ this.handler = handler;
+ }
+
+ TestIndexingMode(String label, IndexingMode mode) {
+ this.label = label;
+ this.mode = mode;
+ }
+}
diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/TestJdkVersion.java b/src/test/scala/org/jetbrains/plugins/scala/util/runners/TestJdkVersion.java
new file mode 100644
index 00000000..39b4523b
--- /dev/null
+++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/TestJdkVersion.java
@@ -0,0 +1,27 @@
+package org.jetbrains.plugins.scala.util.runners;
+
+
+import com.intellij.pom.java.LanguageLevel;
+
+// required at compile time to use in annotations
+public enum TestJdkVersion {
+
+ JDK_1_8, JDK_11, JDK_17;
+
+ public LanguageLevel toProductionVersion() {
+ return switch (this) {
+ case JDK_1_8 -> LanguageLevel.JDK_1_8;
+ case JDK_11 -> LanguageLevel.JDK_11;
+ case JDK_17 -> LanguageLevel.JDK_17;
+ };
+ };
+
+ public static TestJdkVersion from(LanguageLevel level) {
+ return switch (level) {
+ case JDK_1_8 -> JDK_1_8;
+ case JDK_11 -> JDK_11;
+ case JDK_17 -> JDK_17;
+ default -> throw new RuntimeException("Jdk is not supported in tests for now");
+ };
+ }
+}
diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/TestScalaVersion.java b/src/test/scala/org/jetbrains/plugins/scala/util/runners/TestScalaVersion.java
new file mode 100644
index 00000000..e8155a7b
--- /dev/null
+++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/TestScalaVersion.java
@@ -0,0 +1,43 @@
+package org.jetbrains.plugins.scala.util.runners;
+
+import org.jetbrains.plugins.scala.LatestScalaVersions;
+
+// required at compile time to use in annotations
+public enum TestScalaVersion {
+
+ Scala_2_10_0, Scala_2_10_6, Scala_2_10,
+ Scala_2_11_0, Scala_2_11,
+ Scala_2_12_0, Scala_2_12_6, Scala_2_12_12, Scala_2_12,
+ Scala_2_13_0, Scala_2_13,
+ Scala_3_0,
+ Scala_3_1,
+ Scala_3_2,
+ Scala_3_3,
+ Scala_3_4,
+ Scala_3_Latest,
+ Scala_3_Latest_RC
+ ;
+
+ public org.jetbrains.plugins.scala.ScalaVersion toProductionVersion() {
+ return switch (this) {
+ case Scala_2_10 -> LatestScalaVersions.Scala_2_10();
+ case Scala_2_11 -> LatestScalaVersions.Scala_2_11();
+ case Scala_2_12 -> LatestScalaVersions.Scala_2_12();
+ case Scala_2_12_6 -> LatestScalaVersions.Scala_2_12().withMinor(6);
+ case Scala_2_12_12 -> LatestScalaVersions.Scala_2_12().withMinor(12);
+ case Scala_2_13 -> LatestScalaVersions.Scala_2_13();
+ case Scala_2_10_0 -> LatestScalaVersions.Scala_2_10().withMinor(0);
+ case Scala_2_10_6 -> LatestScalaVersions.Scala_2_10().withMinor(6);
+ case Scala_2_11_0 -> LatestScalaVersions.Scala_2_11().withMinor(0);
+ case Scala_2_12_0 -> LatestScalaVersions.Scala_2_12().withMinor(0);
+ case Scala_2_13_0 -> LatestScalaVersions.Scala_2_13().withMinor(0);
+ case Scala_3_0 -> LatestScalaVersions.Scala_3_0();
+ case Scala_3_1 -> LatestScalaVersions.Scala_3_1();
+ case Scala_3_2 -> LatestScalaVersions.Scala_3_2();
+ case Scala_3_3 -> LatestScalaVersions.Scala_3_3();
+ case Scala_3_4 -> LatestScalaVersions.Scala_3_4();
+ case Scala_3_Latest -> LatestScalaVersions.Scala_3();
+ case Scala_3_Latest_RC -> LatestScalaVersions.Scala_3_RC();
+ };
+ }
+}