From b939973def0288795cc30b3ffc2b017484cccbf2 Mon Sep 17 00:00:00 2001 From: Igal Tabachnik Date: Fri, 14 Jun 2024 10:57:22 +0300 Subject: [PATCH] Test fixtures upgrade --- build.sbt | 5 +- ...st.scala => ScalaInspectionTestBase.scala} | 0 .../ScalaLightProjectDescriptor.scala | 56 --- .../SynteticInjectorsTestUtils.scala | 123 ------ .../testfixtures/TestFixtureProvider.scala | 13 - .../scala/intellij/testfixtures/Timings.java | 49 --- .../plugins/scala/annotator/Message.scala | 18 + .../scala/base/EditorActionTestBase.scala | 220 ++++++++++ .../plugins/scala/base}/FailableTest.scala | 11 +- .../scala/base/HelperFixtureEditorOps.scala | 47 +++ .../plugins/scala/base}/InjectableJdk.scala | 0 .../plugins/scala/base}/LibrariesOwner.scala | 0 .../base}/ScalaCodeInsightTestFixture.scala | 0 .../plugins/scala/base/ScalaCodeParsing.scala | 69 ++++ .../ScalaCompletionAutoPopupTestCase.scala | 41 ++ .../scala/base/ScalaFileSetTestCase.java | 388 ++++++++++++++++++ .../scala/base/ScalaFixtureTestCase.scala | 34 ++ ...ScalaLightCodeInsightFixtureTestCase.scala | 0 .../base/ScalaLightProjectDescriptor.scala | 35 ++ .../plugins/scala/base}/ScalaSdkOwner.scala | 85 ++-- .../scala/base}/ScalaVersionProvider.scala | 1 + .../scala/base/SharedTestProjectToken.scala | 26 ++ .../plugins/scala/base/SimpleTestCase.scala | 61 +++ .../libraryLoaders}/IvyManagedLoader.scala | 34 +- .../base/libraryLoaders}/LibraryLoader.scala | 3 - .../libraryLoaders/MockScalaSDKLoader.scala | 46 +++ .../libraryLoaders/ScalaLibraryLoader.scala | 90 ++++ .../ScalaReflectLibraryLoader.scala | 20 + .../base/libraryLoaders}/ScalaSDKLoader.scala | 97 +++-- .../base/libraryLoaders}/SmartJDKLoader.scala | 30 +- .../base/libraryLoaders}/SourcesLoader.scala | 0 .../ThirdPartyLibraryLoader.scala | 30 ++ .../plugins/scala}/testCategories.scala | 9 +- .../plugins/scala/util/AliasExports.scala | 17 + .../plugins/scala/util/BitMaskTest.scala | 99 +++++ .../plugins/scala/util/CompilerTestUtil.scala | 92 +++++ .../scala/util/ConfigureJavaFile.scala | 24 ++ .../scala/util/EditorHintFixtureEx.scala | 26 ++ .../plugins/scala/util/EnumSetTest.scala | 44 ++ .../plugins/scala/util/FindCaretOffset.scala | 38 ++ .../util/GeneratedTestSuiteFactory.scala | 113 +++++ .../plugins/scala/util/HashBuilderTest.scala | 29 ++ .../plugins/scala/util/IconUtils.scala | 42 ++ .../plugins/scala/util/JavaEnum.java | 5 + .../plugins/scala/util}/Markers.scala | 27 +- .../util/ModificationTrackerTester.scala | 27 ++ .../plugins/scala/util/NotNothing.scala | 29 ++ .../plugins/scala/util/PsiFileTestUtil.scala | 45 ++ .../plugins/scala/util/PsiSelectionUtil.scala | 56 +++ .../plugins/scala/util/RevertableChange.scala | 213 ++++++++++ .../plugins/scala/util/ShortCaretMarker.scala | 9 + .../plugins/scala/util/SoftAssert.scala | 32 ++ .../plugins/scala/util}/TestUtils.scala | 0 .../plugins/scala/util/TextRangeUtils.scala | 13 + .../scala/util/TypeAnnotationSettings.scala | 83 ++++ .../scala/util/WriteCommandActionEx.scala | 11 + .../util/assertions/AssertionMatchers.scala | 23 ++ .../assertions/CollectionsAssertions.scala | 22 + .../util/assertions/ExceptionAssertions.scala | 46 +++ .../util/assertions/MatcherAssertions.scala | 61 +++ .../scala/util/assertions/PsiAssertions.scala | 30 ++ .../util/assertions/StringAssertions.scala | 64 +++ .../scala/util/assertions/package.scala | 19 + .../TestDependencyManager.scala | 11 + .../TestDependencyManagerForSbt.scala | 30 ++ .../plugins/scala/util/extensions.scala | 26 ++ .../util/matchers/HamcrestMatchers.scala | 65 +++ .../util/matchers/ScalaBaseMatcher.scala | 29 ++ .../runners/MultipleScalaVersionsRunner.scala | 331 +++++++++++++++ .../util/runners/RunWithAllIndexingModes.java | 23 ++ .../util/runners/RunWithJdkVersions.java | 16 + .../runners/RunWithJdkVersionsFilter.java | 16 + .../util/runners/RunWithScalaVersions.java | 14 + .../runners/RunWithScalaVersionsFilter.java | 16 + .../ScalaVersionAwareTestsCollector.scala | 165 ++++++++ .../scala/util/runners/TestIndexingMode.java | 35 ++ .../scala/util/runners/TestJdkVersion.java | 27 ++ .../scala/util/runners/TestScalaVersion.java | 43 ++ 78 files changed, 3473 insertions(+), 354 deletions(-) rename src/test/scala/intellij/testfixtures/{OperationsOnCollectionInspectionTest.scala => ScalaInspectionTestBase.scala} (100%) delete mode 100644 src/test/scala/intellij/testfixtures/ScalaLightProjectDescriptor.scala delete mode 100644 src/test/scala/intellij/testfixtures/SynteticInjectorsTestUtils.scala delete mode 100644 src/test/scala/intellij/testfixtures/TestFixtureProvider.scala delete mode 100644 src/test/scala/intellij/testfixtures/Timings.java create mode 100644 src/test/scala/org/jetbrains/plugins/scala/annotator/Message.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/EditorActionTestBase.scala rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base}/FailableTest.scala (58%) create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/HelperFixtureEditorOps.scala rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base}/InjectableJdk.scala (100%) rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base}/LibrariesOwner.scala (100%) rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base}/ScalaCodeInsightTestFixture.scala (100%) create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/ScalaCodeParsing.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/ScalaCompletionAutoPopupTestCase.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/ScalaFileSetTestCase.java create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/ScalaFixtureTestCase.scala rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base}/ScalaLightCodeInsightFixtureTestCase.scala (100%) create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/ScalaLightProjectDescriptor.scala rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base}/ScalaSdkOwner.scala (54%) rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base}/ScalaVersionProvider.scala (69%) create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/SharedTestProjectToken.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/SimpleTestCase.scala rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base/libraryLoaders}/IvyManagedLoader.scala (67%) rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base/libraryLoaders}/LibraryLoader.scala (90%) create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/MockScalaSDKLoader.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaLibraryLoader.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaReflectLibraryLoader.scala rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base/libraryLoaders}/ScalaSDKLoader.scala (55%) rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base/libraryLoaders}/SmartJDKLoader.scala (88%) rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/base/libraryLoaders}/SourcesLoader.scala (100%) create mode 100644 src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ThirdPartyLibraryLoader.scala rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala}/testCategories.scala (89%) create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/AliasExports.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/BitMaskTest.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/CompilerTestUtil.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/ConfigureJavaFile.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/EditorHintFixtureEx.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/EnumSetTest.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/FindCaretOffset.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/GeneratedTestSuiteFactory.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/HashBuilderTest.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/IconUtils.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/JavaEnum.java rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/util}/Markers.scala (91%) create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/ModificationTrackerTester.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/NotNothing.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/PsiFileTestUtil.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/PsiSelectionUtil.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/RevertableChange.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/ShortCaretMarker.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/SoftAssert.scala rename src/test/scala/{intellij/testfixtures => org/jetbrains/plugins/scala/util}/TestUtils.scala (100%) create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/TextRangeUtils.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/TypeAnnotationSettings.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/WriteCommandActionEx.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/assertions/AssertionMatchers.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/assertions/CollectionsAssertions.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/assertions/ExceptionAssertions.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/assertions/MatcherAssertions.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/assertions/PsiAssertions.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/assertions/StringAssertions.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/assertions/package.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/dependencymanager/TestDependencyManager.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/dependencymanager/TestDependencyManagerForSbt.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/extensions.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/matchers/HamcrestMatchers.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/matchers/ScalaBaseMatcher.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/MultipleScalaVersionsRunner.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithAllIndexingModes.java create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithJdkVersions.java create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithJdkVersionsFilter.java create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithScalaVersions.java create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithScalaVersionsFilter.java create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/ScalaVersionAwareTestsCollector.scala create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/TestIndexingMode.java create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/TestJdkVersion.java create mode 100644 src/test/scala/org/jetbrains/plugins/scala/util/runners/TestScalaVersion.java diff --git a/build.sbt b/build.sbt index d1cccc85..d37f00c9 100644 --- a/build.sbt +++ b/build.sbt @@ -63,7 +63,10 @@ def newProject(projectName: String, base: File): Project = name := projectName, scalaVersion := scala213, version := pluginVersion, - libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test, + libraryDependencies ++= Seq( + "com.novocode" % "junit-interface" % "0.11" % Test, + "org.junit.jupiter" % "junit-jupiter-api" % "5.10.2" % Test + ), testOptions += Tests.Argument(TestFrameworks.JUnit, "-v", "-s", "-a", "+c", "+q"), intellijPlugins := Seq( "com.intellij.java".toPlugin, diff --git a/src/test/scala/intellij/testfixtures/OperationsOnCollectionInspectionTest.scala b/src/test/scala/intellij/testfixtures/ScalaInspectionTestBase.scala similarity index 100% rename from src/test/scala/intellij/testfixtures/OperationsOnCollectionInspectionTest.scala rename to src/test/scala/intellij/testfixtures/ScalaInspectionTestBase.scala diff --git a/src/test/scala/intellij/testfixtures/ScalaLightProjectDescriptor.scala b/src/test/scala/intellij/testfixtures/ScalaLightProjectDescriptor.scala deleted file mode 100644 index ab6bc1b8..00000000 --- a/src/test/scala/intellij/testfixtures/ScalaLightProjectDescriptor.scala +++ /dev/null @@ -1,56 +0,0 @@ -package org.jetbrains.plugins.scala.base - -import com.intellij.openapi.module.{Module, ModuleManager} -import com.intellij.openapi.project.Project -import com.intellij.testFramework.LightProjectDescriptor - -// TODO: review all usages ScalaLightProjectDescriptor and decide which test classes can reuse test project -class ScalaLightProjectDescriptor(private val sharedProjectToken: SharedTestProjectToken = SharedTestProjectToken.DoNotShare) extends LightProjectDescriptor { - - override def setUpProject(project: Project, handler: LightProjectDescriptor.SetupHandler): Unit = { - super.setUpProject(project, handler) - val modules = ModuleManager.getInstance(project).getModules - tuneModule(modules.head, project) - } - - /** We also pass project because `getProject` in test classes might still be not-initialized (null) */ - def tuneModule(module: Module, project: Project): Unit = () - - /** see [[com.intellij.testFramework.LightPlatformTestCase.doSetup]] */ - override def equals(obj: Any): Boolean = - obj match { - case other: ScalaLightProjectDescriptor => - val equals = for { - id1 <- this.sharedProjectToken.value - id2 <- other.sharedProjectToken.value - } yield id1 == id2 - equals.getOrElse(false) - case _ => - super.equals(obj) - } -} - -/** - * Test cases with the same Some(token) will share test project if run one by-one.
- * This can make each test case initialization significantly faster.
- * If you do not want the project to be shared the token should be None or some other unique value. - * - * @note Suppose test classes A and B use token T1, and test C uses token T2.
- * If test are run in following order: A, C, B, then project will not be reused between A and B. - * (This is because under the hood IntelliJ platform uses a singleton for storing current test project) - */ -case class SharedTestProjectToken(value: Option[AnyRef]) - -object SharedTestProjectToken { - def apply(value: AnyRef): SharedTestProjectToken = - new SharedTestProjectToken(Some(value)) - - val DoNotShare: SharedTestProjectToken = - SharedTestProjectToken(None) - - def ByScalaSdkAndProjectLibraries(test: LibrariesOwner with ScalaSdkOwner): SharedTestProjectToken = - SharedTestProjectToken((test.version, test.librariesLoadersPublic)) - - def ByTestClassAndScalaSdkAndProjectLibraries(test: LibrariesOwner with ScalaSdkOwner): SharedTestProjectToken = - SharedTestProjectToken((test.getClass, test.version, test.librariesLoadersPublic)) -} diff --git a/src/test/scala/intellij/testfixtures/SynteticInjectorsTestUtils.scala b/src/test/scala/intellij/testfixtures/SynteticInjectorsTestUtils.scala deleted file mode 100644 index a21ca3cb..00000000 --- a/src/test/scala/intellij/testfixtures/SynteticInjectorsTestUtils.scala +++ /dev/null @@ -1,123 +0,0 @@ -package org.jetbrains.plugins.scala.lang.macros - -import com.intellij.openapi.diagnostic.Logger -import org.jetbrains.plugins.scala.lang.psi.api.ScalaPsiElement -import org.jetbrains.plugins.scala.lang.psi.api.statements.ScFunction -import org.jetbrains.plugins.scala.lang.psi.api.toplevel.typedef.{ScClass, ScObject, ScTrait, ScTypeDefinition} -import org.jetbrains.plugins.scala.lang.resolve.MethodTypeProvider._ -import org.junit.Assert._ - -object SynteticInjectorsTestUtils { - private val log = Logger.getInstance(getClass) - - sealed trait TDefKind - - object TDefKind { - case object Object extends TDefKind - case object Trait extends TDefKind - case object Class extends TDefKind - } - - sealed trait SyntheticElement { - def validate(e: ScalaPsiElement, strict: Boolean): Unit - } - - final protected case class SyntheticTypeDef( - sig: String, - kind: TDefKind, - functions: Seq[SyntheticMethod] = Seq.empty, - inners: Seq[SyntheticTypeDef] = Seq.empty - ) extends SyntheticElement { - - def `with`(member: SyntheticElement): SyntheticTypeDef = member match { - case m: SyntheticMethod => copy(functions = functions :+ m) - case tdef: SyntheticTypeDef => copy(inners = inners :+ tdef) - } - - def apply(inners: SyntheticElement*): SyntheticTypeDef = - inners.foldLeft(this)(_.`with`(_)) - - def validateInner( - target: ScTypeDefinition, - strict: Boolean - ): Unit = { - val targetFunctions = target.functions ++ target.syntheticMethods - val targetInners = target.allInnerTypeDefinitions - - if (strict && functions.size < targetFunctions.size) { - log.error(s"${targetFunctions.size - functions.size} extra function definition(s) ins ${target.getText}.") - } else - functions.foreach { f => - val funExists = targetFunctions.exists(_.toSynthetic == f) - assertTrue(s"Missing function definition $f in ${target.getText}", funExists) - } - - if (strict && inners.size < targetInners.size) { - log.error(s"${targetInners.size - inners.size} extra type definition(s) ins ${target.getText}.") - } else - inners.foreach { inner => - val maybeInner = targetInners.find(_.sig == inner.sig) - assertTrue(s"Missing type definition $inner in ${target.getText}", maybeInner.isDefined) - maybeInner.foreach(inner.validate(_, strict)) - } - } - - private def checkSig(tdef: ScTypeDefinition): Unit = - assertEquals("Type definition signature doesn't match expected", sig, tdef.sig) - - override def validate(e: ScalaPsiElement, strict: Boolean): Unit = (e, kind) match { - case (o: ScObject, TDefKind.Object) => checkSig(o); validateInner(o, strict) - case (c: ScClass, TDefKind.Class) => checkSig(c); validateInner(c, strict) - case (t: ScTrait, TDefKind.Trait) => checkSig(t); validateInner(t, strict) - case _ => throw new IllegalArgumentException - } - } - - final case class SyntheticMethod( - name: String, - tpeSig: String, - isImplicit: Boolean - ) extends SyntheticElement { - - override def validate(e: ScalaPsiElement, strict: Boolean): Unit = e match { - case f: ScFunction => assertEquals(s"Failed to validate function $f against $this", this, f.toSynthetic) - case _ => throw new IllegalArgumentException - } - } - - def `object`(sig: String): SyntheticTypeDef = - SyntheticTypeDef(sig, TDefKind.Object) - - def `class`(sig: String): SyntheticTypeDef = - SyntheticTypeDef(sig, TDefKind.Class) - - def `trait`(sig: String): SyntheticTypeDef = - SyntheticTypeDef(sig, TDefKind.Trait) - - def `def`(name: String, tpe: String): SyntheticMethod = - SyntheticMethod(name, tpe, isImplicit = false) - - def `implicit`(name: String, tpe: String): SyntheticMethod = - SyntheticMethod(name, tpe, isImplicit = true) - - implicit class SyntheticChecker(private val target: ScalaPsiElement) extends AnyVal { - def mustBeLike(synthetic: SyntheticElement): Unit = synthetic.validate(target, strict = false) - def mustBeExactly(synthetic: SyntheticElement): Unit = synthetic.validate(target, strict = true) - } - - implicit class ScTypeDefSig(private val tdef: ScTypeDefinition) extends AnyVal { - - def sig: String = { - val supers = tdef.extendsBlock.templateParents.fold("")(" extends " + _.getText) - val name = tdef.name - val tparams = tdef.typeParametersClause.fold("")(_.getText) - s"$name$tparams$supers" - } - } - - implicit class ScFunctionToSynthetic(private val f: ScFunction) extends AnyVal { - - def toSynthetic: SyntheticMethod = - SyntheticMethod(f.name, f.polymorphicType().canonicalText, f.hasModifierPropertyScala("implicit")) - } -} diff --git a/src/test/scala/intellij/testfixtures/TestFixtureProvider.scala b/src/test/scala/intellij/testfixtures/TestFixtureProvider.scala deleted file mode 100644 index 46c28f71..00000000 --- a/src/test/scala/intellij/testfixtures/TestFixtureProvider.scala +++ /dev/null @@ -1,13 +0,0 @@ -package org.jetbrains.plugins.scala - -import com.intellij.testFramework.fixtures.CodeInsightTestFixture - -/** - * @author mucianm - * @since 07.04.16. - */ -trait TestFixtureProvider { - def getFixture: CodeInsightTestFixture - - // implicit final def module: Module = getFixture.getModule -} diff --git a/src/test/scala/intellij/testfixtures/Timings.java b/src/test/scala/intellij/testfixtures/Timings.java deleted file mode 100644 index f17bb415..00000000 --- a/src/test/scala/intellij/testfixtures/Timings.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2000-2005 by JetBrains s.r.o. All Rights Reserved. - * Use is subject to license terms. - */ -package intellij.testfixtures; - -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.math.BigInteger; - -/** - * @author peter - */ -public class Timings { - - static { - long start = System.currentTimeMillis(); - BigInteger k = new BigInteger("1"); - for (int i = 0; i < 1000000; i++) { - k = k.add(new BigInteger("1")); - } - for (int i = 0; i < 15; i++) { - try { - final File tempFile = File.createTempFile("test", "test" + i); - final FileWriter writer = new FileWriter(tempFile); - for (int j = 0; j < 15; j++) { - writer.write("test" + j); - writer.flush(); - } - writer.close(); - final FileReader reader = new FileReader(tempFile); - while (reader.read() >= 0) {} - reader.close(); - tempFile.delete(); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - - MACHINE_TIMING = System.currentTimeMillis() - start; - } - - public static final long MACHINE_TIMING; - - -} diff --git a/src/test/scala/org/jetbrains/plugins/scala/annotator/Message.scala b/src/test/scala/org/jetbrains/plugins/scala/annotator/Message.scala new file mode 100644 index 00000000..3d6a6fcf --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/annotator/Message.scala @@ -0,0 +1,18 @@ +package org.jetbrains.plugins.scala.annotator + +import scala.math.Ordered.orderingToOrdered + +sealed abstract class Message extends Ordered[Message] { + def element: String + def message: String + + override def compare(that: Message): Int = + (this.element, this.message) compare (that.element, that.message) +} + +object Message { + case class Info(override val element: String, override val message: String) extends Message + case class Warning(override val element: String, override val message: String) extends Message + case class Error(override val element: String, override val message: String) extends Message + case class Hint(override val element: String, text: String, override val message: String = "", offsetDelta: Int = 0) extends Message +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/EditorActionTestBase.scala b/src/test/scala/org/jetbrains/plugins/scala/base/EditorActionTestBase.scala new file mode 100644 index 00000000..7736f714 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/EditorActionTestBase.scala @@ -0,0 +1,220 @@ +package org.jetbrains.plugins.scala +package base + +import com.intellij.openapi.actionSystem.IdeActions.{ACTION_EDITOR_BACKSPACE, ACTION_EDITOR_ENTER, ACTION_EXPAND_LIVE_TEMPLATE_BY_TAB} +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.impl.NonBlockingReadActionImpl +import com.intellij.openapi.editor.CaretState +import com.intellij.openapi.editor.ex.util.EditorUtil +import com.intellij.openapi.editor.impl.{DocumentImpl, TrailingSpacesStripper} +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageEditorUtil +import com.intellij.testFramework.fixtures.IdeaTestExecutionPolicy +import com.intellij.util.ui.UIUtil +import org.jetbrains.plugins.scala.ScalaFileType +import org.jetbrains.plugins.scala.editor.DocumentExt +import org.jetbrains.plugins.scala.extensions.{inWriteCommandAction, startCommand} +import org.jetbrains.plugins.scala.util.FindCaretOffset.findCaretOffsets +import org.jetbrains.plugins.scala.util.ShortCaretMarker +import org.jetbrains.plugins.scala.util.extensions.ComparisonFailureOps +import org.junit.Assert._ +import org.junit.experimental.categories.Category + +import scala.jdk.CollectionConverters._ +import scala.util.control.NonFatal + +@Category(Array(classOf[EditorTests])) +abstract class EditorActionTestBase extends ScalaLightCodeInsightFixtureTestCase with ShortCaretMarker { + + protected val q : String = "\"" + protected val qq : String = "\"\"" + protected val qqq: String = "\"\"\"" + + private implicit def p: Project = getProject + + protected def fileType: FileType = ScalaFileType.INSTANCE + + protected def defaultFileName: String = s"aaa.${fileType.getDefaultExtension}" + + protected def configureByText(text: String, + fileName: String = defaultFileName, + trimText: Boolean = false): Unit = { + val (textActual, caretOffsets) = findCaretOffsets(text, trimText) + + assertTrue("expected at least one caret", caretOffsets.nonEmpty) + + myFixture.getEditor match { + case null => + myFixture.configureByText(fileName, textActual) + case editor => + // optimization for sequential this.configureByText calls in a single test + // myFixture.configureByText is quite resource consuming for simple sequence of typing tests + inWriteCommandAction { + editor.getDocument.setText(textActual) + editor.getDocument.commit(getProject) + } + } + val editor = myFixture.getEditor + editor.getCaretModel.moveToOffset(caretOffsets.head) + val caretStates = caretOffsets.map { offset => new CaretState(editor.offsetToLogicalPosition(offset), null, null) } + editor.getCaretModel.setCaretsAndSelections(caretStates.asJava) + } + + /** + * @param textBefore editor text with caret markers before the action + * @param textAfter editor text with caret markers after the action + * @param stripTrailingSpacesAfterAction whether to trim trailing editor spaces after action perform + * @param testBody action to perform with `textBefore` + */ + protected def performTest( + textBefore: String, + textAfter: String, + fileName: String = defaultFileName, + trimTestDataText: Boolean = false, + stripTrailingSpacesAfterAction: Boolean = false, + )(testBody: () => Unit): Unit = try { + configureByText(textBefore, fileName, trimTestDataText) + + testBody() + + val (expectedText, expectedCarets) = findCaretOffsets(textAfter, trimTestDataText) + + /** + * Copied from `com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl.checkResult` + * Replaced inner `checkResult` call with `checkCaretOffsets` + * It allows to see caret positions together with file text directly in the diff view of failed test + * It's more convenient then operating with caret offset (as simple integer value) + */ + Option(IdeaTestExecutionPolicy.current).foreach(_.beforeCheckResult(getFile)) + inWriteCommandAction { + PsiDocumentManager.getInstance(getProject).commitAllDocuments() + EditorUtil.fillVirtualSpaceUntilCaret(InjectedLanguageEditorUtil.getTopLevelEditor(getEditor)) + + checkTextWithCaretOffsets(expectedCarets, expectedText, stripTrailingSpacesAfterAction) + } + } catch { + case cf: org.junit.ComparisonFailure => + throw cf.withBeforePrefix(textBefore) + + case NonFatal(other) => + System.err.println( + s"""<<>> + |$textBefore""".stripMargin + ) + throw other + } + + protected def performTypingAction(charTyped: Char): Unit = + myFixture.`type`(charTyped) + + protected def performTypingAction(text: String): Unit = + myFixture.`type`(text) + + protected def checkGeneratedTextAfterTyping(textBefore: String, textAfter: String, charTyped: Char, + fileName: String = defaultFileName): Unit = + performTest(textBefore, textAfter, fileName) { () => + performTypingAction(charTyped) + } + + protected def checkGeneratedTextAfterTypingText(textBefore: String, textAfter: String, textTyped: String, + fileName: String = defaultFileName): Unit = + performTest(textBefore, textAfter, fileName) { () => + performTypingAction(textTyped) + } + + protected def checkGeneratedTextAfterTypingTextCharByChar( + textBefore: String, + textAfter: String, + textTyped: String, + fileName: String = defaultFileName + ): Unit = + performTest(textBefore, textAfter, fileName) { () => + textTyped.foreach { char: Char => + performTypingAction(char) + getEditor.getDocument.commit(getProject) + } + } + + protected def performBackspaceAction(): Unit = + performEditorAction(ACTION_EDITOR_BACKSPACE) + + protected def checkGeneratedTextAfterBackspace(textBefore: String, textAfter: String): Unit = + performTest(textBefore, textAfter) { () => + performBackspaceAction() + } + + protected def performEnterAction(): Unit = + performEditorAction(ACTION_EDITOR_ENTER) + + protected def checkGeneratedTextAfterEnter(textBefore: String, textAfter: String): Unit = + performTest(textBefore, textAfter) { () => + performEnterAction() + } + + protected def performLiveTemplateAction(): Unit = + performEditorAction(ACTION_EXPAND_LIVE_TEMPLATE_BY_TAB) + + protected def checkGeneratedTextAfterLiveTemplate(textBefore: String, textAfter: String): Unit = + performTest(textBefore, textAfter) { () => + performLiveTemplateAction() + + if (ApplicationManager.getApplication.isDispatchThread) { + NonBlockingReadActionImpl.waitForAsyncTaskCompletion() + UIUtil.dispatchAllInvocationEvents() + } + } + + private def performEditorAction(action: String): Unit = + startCommand() { + myFixture.performEditorAction(action) + }(getProject) + + protected def checkTextWithCaretOffsets( + expectedCarets: Seq[Int], + expectedText: String, + stripTrailingSpaces: Boolean + ): Unit = { + val document = myFixture.getDocument(myFixture.getFile).asInstanceOf[DocumentImpl] + if (stripTrailingSpaces) { + TrailingSpacesStripper.strip(document, false, true) + } + + val allCaretOffsets = + myFixture.getEditor.getCaretModel.getAllCarets.asScala.iterator.map(_.getOffset).toSeq + + checkTextWithCaretOffsets( + expectedCarets, + allCaretOffsets, + expectedText, + document.getText, + stripTrailingSpaces + ) + } + + private def checkTextWithCaretOffsets( + expectedCarets: Seq[Int], + actualCarets: Seq[Int], + expectedText: String, + actualText: String, + stripTrailingSpaces: Boolean + ): Unit = { + def doStripTrailingSpaces(text: String): String = + text.replaceAll(" +\n", "\n") + + def patchTextWithCarets(text: String, caretOffsets: Seq[Int]): String = + caretOffsets + .sorted(Ordering.Int.reverse) + .foldLeft(text)(_.patch(_, CARET, 0)) + + val expected0 = patchTextWithCarets(expectedText, expectedCarets) + val expected = if (stripTrailingSpaces) doStripTrailingSpaces(expected0) else expected0 + + val actual = if (expectedCarets.nonEmpty) + patchTextWithCarets(actualText, actualCarets) + else + actualText //if expected text doesn't contain any carets, just don't assert carets positions then + assertEquals(expected, actual) + } +} diff --git a/src/test/scala/intellij/testfixtures/FailableTest.scala b/src/test/scala/org/jetbrains/plugins/scala/base/FailableTest.scala similarity index 58% rename from src/test/scala/intellij/testfixtures/FailableTest.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/FailableTest.scala index c8ea83bf..135125fc 100644 --- a/src/test/scala/intellij/testfixtures/FailableTest.scala +++ b/src/test/scala/org/jetbrains/plugins/scala/base/FailableTest.scala @@ -5,14 +5,15 @@ import org.junit.Assert trait FailableTest { /** - * A hook to allow tests that are currently failing to pass when they fail and vice versa. - * @return - */ + * A hook to allow tests that are currently failing to pass when they fail and vice versa. + * @return + */ protected def shouldPass: Boolean = true - protected def assertEqualsFailable(expected: AnyRef, actual: AnyRef): Unit = + protected def assertEqualsFailable(expected: AnyRef, actual: AnyRef): Unit = { if (shouldPass) Assert.assertEquals(expected, actual) else Assert.assertNotEquals(expected, actual) + } - val failingPassed: String = "Test has passed, but was supposed to fail" + protected val failingPassed: String = "Test has passed, but was supposed to fail" } diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/HelperFixtureEditorOps.scala b/src/test/scala/org/jetbrains/plugins/scala/base/HelperFixtureEditorOps.scala new file mode 100644 index 00000000..f0ddd223 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/HelperFixtureEditorOps.scala @@ -0,0 +1,47 @@ +package org.jetbrains.plugins.scala.base + +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture +import org.jetbrains.plugins.scala.extensions.{inWriteCommandAction, invokeAndWait} +import org.jetbrains.plugins.scala.settings.ScalaApplicationSettings + +trait HelperFixtureEditorOps { + protected def getFixture: JavaCodeInsightTestFixture + + protected def getProject: Project + + final def commitDocumentInEditor(): Unit = { + val documentManager = PsiDocumentManager.getInstance(getProject) + documentManager.commitDocument(getFixture.getEditor.getDocument) + } + + final def changePsiAt(offset: Int): Unit = { + val settings = ScalaApplicationSettings.getInstance() + val oldAutoBraceSettings = settings.HANDLE_BLOCK_BRACES_INSERTION_AUTOMATICALLY + settings.HANDLE_BLOCK_BRACES_INSERTION_AUTOMATICALLY = false + try { + typeAndRemoveChar(offset, 'a') + } finally { + settings.HANDLE_BLOCK_BRACES_INSERTION_AUTOMATICALLY = oldAutoBraceSettings + } + } + + protected def typeAndRemoveChar(offset: Int, charToTypeAndRemove: Char): Unit = invokeAndWait { + getFixture.getEditor.getCaretModel.moveToOffset(offset) + getFixture.`type`(charToTypeAndRemove) + commitDocumentInEditor() + getFixture.performEditorAction(IdeActions.ACTION_EDITOR_BACKSPACE) + commitDocumentInEditor() + } + + protected def insertAtOffset(offset: Int, text: String): Unit = { + invokeAndWait { + inWriteCommandAction { + getFixture.getEditor.getDocument.insertString(offset, text) + commitDocumentInEditor() + }(getProject) + } + } +} diff --git a/src/test/scala/intellij/testfixtures/InjectableJdk.scala b/src/test/scala/org/jetbrains/plugins/scala/base/InjectableJdk.scala similarity index 100% rename from src/test/scala/intellij/testfixtures/InjectableJdk.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/InjectableJdk.scala diff --git a/src/test/scala/intellij/testfixtures/LibrariesOwner.scala b/src/test/scala/org/jetbrains/plugins/scala/base/LibrariesOwner.scala similarity index 100% rename from src/test/scala/intellij/testfixtures/LibrariesOwner.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/LibrariesOwner.scala diff --git a/src/test/scala/intellij/testfixtures/ScalaCodeInsightTestFixture.scala b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaCodeInsightTestFixture.scala similarity index 100% rename from src/test/scala/intellij/testfixtures/ScalaCodeInsightTestFixture.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/ScalaCodeInsightTestFixture.scala diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/ScalaCodeParsing.scala b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaCodeParsing.scala new file mode 100644 index 00000000..37d8fe44 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaCodeParsing.scala @@ -0,0 +1,69 @@ +package org.jetbrains.plugins.scala.base + +import com.intellij.psi.PsiElement +import org.intellij.lang.annotations.{Language => InputLanguage} +import org.jetbrains.plugins.scala.ScalaVersion +import org.jetbrains.plugins.scala.extensions.{IteratorExt, PsiElementExt} +import org.jetbrains.plugins.scala.lang.psi.api.ScalaFile +import org.jetbrains.plugins.scala.lang.psi.impl.ScalaPsiElementFactory +import org.jetbrains.plugins.scala.project.{ProjectContext, ScalaFeatures} + +import scala.reflect.ClassTag + +trait ScalaCodeParsing { + + protected def scalaVersion: ScalaVersion = ScalaVersion.default + + def parseScalaFile( + @InputLanguage("Scala") text: String, + scalaVersion: ScalaVersion + )(implicit project: ProjectContext): ScalaFile = { + parseScalaFile(text, scalaVersion, enableEventSystem = false) + } + + def parseScalaFile( + @InputLanguage("Scala") text: String, + enableEventSystem: Boolean = false + )(implicit project: ProjectContext): ScalaFile = { + parseScalaFile(text, scalaVersion, enableEventSystem) + } + + def parseScalaFileAndGetCaretPosition( + @InputLanguage("Scala") text: String, + caretMarker: String + )(implicit project: ProjectContext): (ScalaFile, Int) = { + val trimmed = text.trim + val caretPos = trimmed.indexOf(caretMarker) + (parseScalaFile(trimmed.replaceAll(caretMarker, "")), caretPos) + } + + private def parseScalaFile( + @InputLanguage("Scala") text: String, + scalaVersion: ScalaVersion, + enableEventSystem: Boolean, + )(implicit project: ProjectContext): ScalaFile = { + val scalaFeatures = ScalaFeatures.onlyByVersion(scalaVersion) + ScalaPsiElementFactory.createScalaFileFromText(text, scalaFeatures, eventSystemEnabled = enableEventSystem, shouldTrimText = false) + } + + implicit class ScalaCode(@InputLanguage("Scala") private val text: String) { + def stripComments: String = + text.replaceAll("""(?s)/\*.*?\*/""", "") + .replaceAll("""(?m)//.*$""", "") + + def parse(implicit project: ProjectContext): ScalaFile = + parseScalaFile(text) + + def parse(scalaVersion: ScalaVersion)(implicit project: ProjectContext): ScalaFile = + parseScalaFile(text, scalaVersion) + + def parseWithEventSystem(implicit project: ProjectContext): ScalaFile = + parseScalaFile(text, enableEventSystem = true) + + def parse[T <: PsiElement : ClassTag](implicit project: ProjectContext): T = + parse(project).depthFirst().findByType[T].getOrElse { + throw new RuntimeException("Unable to find PSI element with type " + + implicitly[ClassTag[T]].runtimeClass.getSimpleName) + } + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/ScalaCompletionAutoPopupTestCase.scala b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaCompletionAutoPopupTestCase.scala new file mode 100644 index 00000000..b37b7c35 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaCompletionAutoPopupTestCase.scala @@ -0,0 +1,41 @@ +package org.jetbrains.plugins.scala.base + +import com.intellij.codeInsight.lookup.impl.LookupImpl +import com.intellij.openapi.fileTypes.FileType +import com.intellij.psi.PsiFile +import com.intellij.testFramework.fixtures.CompletionAutoPopupTester +import com.intellij.util.ThrowableRunnable +import org.jetbrains.plugins.scala.ScalaFileType + +/** @see [[com.intellij.codeInsight.completion.JavaCompletionAutoPopupTestCase]] */ +abstract class ScalaCompletionAutoPopupTestCase extends ScalaLightCodeInsightFixtureTestCase { + private[this] var myTester: CompletionAutoPopupTester = _ + + override protected def setUp(): Unit = { + super.setUp() + myTester = new CompletionAutoPopupTester(myFixture) + } + + override protected def runInDispatchThread(): Boolean = false + + override protected def runTestRunnable(testRunnable: ThrowableRunnable[Throwable]): Unit = + myTester.runWithAutoPopupEnabled(testRunnable) + + protected def getLookup: LookupImpl = myTester.getLookup + + protected def doType(textToType: String): Unit = + myTester.typeWithPauses(textToType) + + protected def fileType: FileType = ScalaFileType.INSTANCE + + private def appendExtension(fileName: String): String = + s"$fileName.${fileType.getDefaultExtension}" + + private def defaultFileName: String = appendExtension("aaa") + + protected def configureByText(text: String): PsiFile = + myFixture.configureByText(defaultFileName, text) + + protected def configureByFile(fileName: String): PsiFile = + myFixture.configureByFile(appendExtension(fileName)) +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/ScalaFileSetTestCase.java b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaFileSetTestCase.java new file mode 100644 index 00000000..99f3a6ac --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaFileSetTestCase.java @@ -0,0 +1,388 @@ +/* + * Copyright 2000-2008 JetBrains s.r.o. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.plugins.scala.base; + +import com.intellij.application.options.CodeStyle; +import com.intellij.lang.Language; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiFileFactory; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; +import com.intellij.util.ThrowableRunnable; +import junit.framework.Test; +import junit.framework.TestSuite; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.scala.FileSetTests; +import org.jetbrains.plugins.scala.ScalaLanguage; +import org.jetbrains.plugins.scala.ScalaVersion; +import org.jetbrains.plugins.scala.lang.formatting.settings.ScalaCodeStyleSettings; +import org.jetbrains.plugins.scala.util.TestUtils; +import org.junit.experimental.categories.Category; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import static com.intellij.openapi.util.io.FileUtil.loadFileText; +import static com.intellij.openapi.util.text.StringUtil.*; +import static com.intellij.psi.impl.DebugUtil.psiToString; +import static org.junit.Assert.*; + +public abstract class ScalaFileSetTestCase extends TestSuite { + + protected ScalaFileSetTestCase(@NotNull @NonNls String path, String... testFileExtensions) { + String pathProperty = System.getProperty("path"); + String customOrPropertyPath = pathProperty != null ? + pathProperty : + getTestDataPath() + path; + + findFiles(new File(customOrPropertyPath)) + .filter(file -> isTestFile(file, testFileExtensions)) + .map(this::constructTestCase) + .forEach(this::addTest); + + assertTrue("No tests found", testCount() > 0); + } + + protected boolean needsSdk() { + return false; + } + + private Test constructTestCase(File file) { + if (needsSdk()) + return new ActualTest(file); + return new NoSdkTestCase(file); + } + + protected void setUp(@NotNull Project project) { + setSettings(project); + } + + protected void tearDown(@NotNull Project project) { + } + + @NotNull + protected String getTestDataPath() { + return TestUtils.getTestDataPath(); + } + + @NotNull + protected Language getLanguage() { + return ScalaLanguage.INSTANCE; + } + + //used just to propagate to ActualTest.supportedIn + //default implementation took from org.jetbrains.plugins.scala.base.ScalaSdkOwner.supportedIn + //TODO: consider using Scala 2.13 by default + protected boolean supportedInScalaVersion(ScalaVersion version) { + return true; + } + + @NotNull + @Override + public final String getName() { + return getClass().getName(); + } + + protected void setSettings(@NotNull Project project) { + CommonCodeStyleSettings.IndentOptions indentOptions = getCommonSettings(project).getIndentOptions(); + assertNotNull(indentOptions); + setIndentSettings(indentOptions); + } + + protected void setIndentSettings(@NotNull CommonCodeStyleSettings.IndentOptions indentOptions) { + indentOptions.INDENT_SIZE = 2; + indentOptions.CONTINUATION_INDENT_SIZE = 2; + indentOptions.TAB_SIZE = 2; + } + + // TODO: make this method abstract and reuse implementation using e.g. mixins in parser tests + // this method builds psi tree string and it's only applicable to parser tests + @NotNull + protected String transform(@NotNull String testName, + @NotNull String fileText, + @NotNull Project project) { + PsiFile lightFile = createLightFile(fileText, project); + + return psiToString(lightFile, true) + .replace(": " + lightFile.getName(), ""); + } + + @NotNull + protected String transformExpectedResult(@NotNull String text) { + return text; + } + + protected void runTest(@NotNull final String testName0, + @NotNull final String content0, + @NotNull final Project project) { + final List input = new ArrayList<>(); + + int separatorIndex; + // Adding input before ----- + String content = content0; + while ((separatorIndex = content.indexOf("-----")) >= 0) { + input.add(content.substring(0, separatorIndex - 1)); + content = content.substring(separatorIndex); + while (startsWithChar(content, '-') || + startsWithChar(content, '\n')) { + content = content.substring(1); + } + } + + // Result - after ----- + String result = content; + while (startsWithChar(result, '-') || + startsWithChar(result, '\n') || + startsWithChar(result, '\r')) { + result = result.substring(1); + } + + if (result.trim().equalsIgnoreCase("UNCHANGED_TAG")) { + assertEquals("Unchanged expected result expects only 1 input entry", 1, input.size()); + result = input.get(0); + } + + assertFalse("No data found in source file", input.isEmpty()); + assertNotNull(result); + + final String testName; + final int dotIdx = testName0.indexOf('.'); + testName = dotIdx >= 0 ? testName0.substring(0, dotIdx) : testName0; + + String temp = transform(testName, input.get(0), project); + result = transformExpectedResult(result.trim()); + + final String transformed = convertLineSeparators(temp).trim(); + + if (shouldPass()) { + assertEquals(result, transformed); + } else { + assertNotEquals(result, transformed); + } + } + + protected boolean shouldPass() { + return true; + } + + @SuppressWarnings("UnconstructableJUnitTestCase") + @Category({FileSetTests.class}) + private final class NoSdkTestCase extends LightJavaCodeInsightFixtureTestCase { + private final File testFile; + + private NoSdkTestCase(@NotNull File testFile) { + this.testFile = testFile; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + ScalaFileSetTestCase.this.setUp(getProject()); + } + + @Override + protected void tearDown() throws Exception { + try { + ScalaFileSetTestCase.this.tearDown(getProject()); + } finally { + super.tearDown(); + } + } + + @Override + public void runTestRunnable(@NotNull ThrowableRunnable testRunnable) throws Throwable { + final var fileText = FileUtil.loadFile(testFile, StandardCharsets.UTF_8); + try { + ScalaFileSetTestCase.this.runTest( + testFile.getName(), + convertLineSeparators(fileText), + getProject() + ); + } catch(Throwable error) { + // to be able to Ctrl + Click in console to nabigate to test file on failure + // (note, can not work with Android plugin disabled, see IDEA-257969) + System.err.println("### Test file: " + testFile.getAbsolutePath()); + throw error; + } + } + + @NotNull + @Override + public String toString() { + return getName(); + } + + @NotNull + @Override + public String getName() { + final var name = testFile.getName(); + final var dotIndex = name.lastIndexOf('.'); + if (dotIndex == -1) { + return name; + } + return name.substring(0, dotIndex); + } + } + + @SuppressWarnings("UnconstructableJUnitTestCase") + @Category({FileSetTests.class}) + private final class ActualTest extends ScalaLightCodeInsightFixtureTestCase { + + private final File myTestFile; + + private ActualTest(@NotNull File testFile) { + myTestFile = testFile; + } + + @Override + public boolean supportedIn(ScalaVersion version) { + return supportedInScalaVersion(version); + } + + @Override + public void setUp() { + try { + super.setUp(); + ScalaFileSetTestCase.this.setUp(getProject()); + TestUtils.disableTimerThread(); + } catch (Exception e) { + try { + tearDown(); + } catch (Exception ignored) { + } + throw e; + } + } + + @Override + public void tearDown() { + ScalaFileSetTestCase.this.tearDown(getProject()); + try { + super.tearDown(); + } catch (IllegalArgumentException ignored) { + } + } + + @Override + public void runTestRunnable(@NotNull ThrowableRunnable testRunnable) throws Throwable { + String fileText = new String(loadFileText(myTestFile, "UTF-8")); + try { + ScalaFileSetTestCase.this.runTest( + myTestFile.getName(), + convertLineSeparators(fileText), + getProject() + ); + } catch(Throwable error) { + // to be able to Ctrl + Click in console to nabigate to test file on failure + // (note, can not work with Android plugin disabled, see IDEA-257969) + System.err.println("### Test file: " + myTestFile.getAbsolutePath()); + throw error; + } + } + + @NotNull + @Override + protected String getTestName(boolean lowercaseFirstLetter) { + return ""; + } + + @NotNull + @Override + public String toString() { + return getName() + " "; + } + + @NotNull + @Override + public String getName() { + return myTestFile.getAbsolutePath(); + } + } + + @NotNull + protected final ScalaCodeStyleSettings getScalaSettings(@NotNull Project project) { + return getSettings(project).getCustomSettings(ScalaCodeStyleSettings.class); + } + + @NotNull + protected final CommonCodeStyleSettings getCommonSettings(@NotNull Project project) { + return getSettings(project).getCommonSettings(ScalaLanguage.INSTANCE); + } + + protected final PsiFile createLightFile(@NotNull @NonNls String text, + @NotNull Project project) { + return PsiFileFactory.getInstance(project).createFileFromText( + "dummy.scala", + getLanguage(), + text + ); + } + + @NotNull + private static CodeStyleSettings getSettings(@NotNull Project project) { + return CodeStyle.getSettings(project); + } + + @NotNull + private static Stream findFiles(@NotNull File baseFile) { + if (baseFile.exists()) { + List myFiles = new ArrayList<>(); + scanForFiles(baseFile, myFiles); + return myFiles.stream(); + } else { + return Stream.empty(); + } + } + + @SuppressWarnings({"HardCodedStringLiteral"}) + private static void scanForFiles(@NotNull File directory, + @NotNull List accumulator) { + // recursively scan for all subdirectories + if (directory.isDirectory()) { + for (File file : Objects.requireNonNull(directory.listFiles())) { + if (file.isDirectory()) { + scanForFiles(file, accumulator); + } else { + accumulator.add(file); + } + } + } + } + + private static boolean isTestFile(@NotNull File file, String[] testFileExtensions) { + String path = file.getAbsolutePath(); + String name = file.getName(); + + if (testFileExtensions.length == 0) { + testFileExtensions = new String[] { ".test" }; + } + + return !path.contains(".svn") && + !path.contains(".cvs") && + Arrays.stream(testFileExtensions).anyMatch(ext -> endsWith(name, ext)) && + !startsWithChar(name, '_') && + !"CVS".equals(name); + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/ScalaFixtureTestCase.scala b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaFixtureTestCase.scala new file mode 100644 index 00000000..787cb320 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaFixtureTestCase.scala @@ -0,0 +1,34 @@ +package org.jetbrains.plugins.scala +package base + +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.registry.Registry +import com.intellij.testFramework.{EditorTestUtil, IndexingTestUtil} +import com.intellij.testFramework.fixtures.CodeInsightFixtureTestCase +import org.jetbrains.plugins.scala.base.libraryLoaders.{HeavyJDKLoader, LibraryLoader, ScalaSDKLoader} + +abstract class ScalaFixtureTestCase extends CodeInsightFixtureTestCase with ScalaSdkOwner { + + protected val CARET = EditorTestUtil.CARET_TAG + + protected val includeCompilerAsLibrary: Boolean = false + + protected final implicit def projectContext: Project = getProject + + override protected def librariesLoaders: Seq[LibraryLoader] = Seq( + ScalaSDKLoader(includeScalaCompilerIntoLibraryClasspath = includeCompilerAsLibrary), + HeavyJDKLoader() + ) + + override protected def setUp(): Unit = { + super.setUp() + setUpLibraries(myModule) + IndexingTestUtil.waitUntilIndexesAreReady(getProject) + Registry.get("ast.loading.filter").setValue(true, getTestRootDisposable) + } + + override def tearDown(): Unit = { + disposeLibraries(myModule) + super.tearDown() + } +} \ No newline at end of file diff --git a/src/test/scala/intellij/testfixtures/ScalaLightCodeInsightFixtureTestCase.scala b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaLightCodeInsightFixtureTestCase.scala similarity index 100% rename from src/test/scala/intellij/testfixtures/ScalaLightCodeInsightFixtureTestCase.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/ScalaLightCodeInsightFixtureTestCase.scala diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/ScalaLightProjectDescriptor.scala b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaLightProjectDescriptor.scala new file mode 100644 index 00000000..b874921b --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaLightProjectDescriptor.scala @@ -0,0 +1,35 @@ +package org.jetbrains.plugins.scala.base + +import com.intellij.openapi.module.{Module, ModuleManager} +import com.intellij.openapi.project.Project +import com.intellij.testFramework.LightProjectDescriptor + +/** + * See other examples: + * - [[com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase.ProjectDescriptor]] + * - [[com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor]] + */ +class ScalaLightProjectDescriptor(private val sharedProjectToken: SharedTestProjectToken = SharedTestProjectToken.DoNotShare) extends LightProjectDescriptor { + + override def setUpProject(project: Project, handler: LightProjectDescriptor.SetupHandler): Unit = { + super.setUpProject(project, handler) + val modules = ModuleManager.getInstance(project).getModules + tuneModule(modules.head, project) + } + + /** We also pass project because `getProject` in test classes might still be not-initialized (null) */ + def tuneModule(module: Module, project: Project): Unit = () + + /** see [[com.intellij.testFramework.LightPlatformTestCase.doSetup]] */ + override def equals(obj: Any): Boolean = + obj match { + case other: ScalaLightProjectDescriptor => + val equals = for { + id1 <- this.sharedProjectToken.value + id2 <- other.sharedProjectToken.value + } yield id1 == id2 + equals.getOrElse(false) + case _ => + super.equals(obj) + } +} diff --git a/src/test/scala/intellij/testfixtures/ScalaSdkOwner.scala b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaSdkOwner.scala similarity index 54% rename from src/test/scala/intellij/testfixtures/ScalaSdkOwner.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/ScalaSdkOwner.scala index b365e7e5..054d3f7f 100644 --- a/src/test/scala/intellij/testfixtures/ScalaSdkOwner.scala +++ b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaSdkOwner.scala @@ -1,7 +1,8 @@ -package org.jetbrains.plugins.scala -package base +package org.jetbrains.plugins.scala.base +import com.intellij.util.containers.ContainerUtil import _root_.junit.framework.{AssertionFailedError, Test, TestListener, TestResult} +import org.jetbrains.plugins.scala.{LatestScalaVersions, ScalaVersion} import scala.collection.immutable.SortedSet @@ -12,14 +13,19 @@ trait ScalaSdkOwner extends Test import ScalaSdkOwner._ - @deprecatedOverriding( - "Consider using supportedIn instead to run with the latest possible scala version.\n" + - "Override this method only if you want to run test with a specific version which is for some reason not listed in ScalaSdkOwner.allTestVersion" - ) - override implicit def version: ScalaVersion = { - val supportedVersions = allTestVersions.filter(supportedIn) - val configuredVersion = configuredScalaVersion.orElse(defaultVersionOverride).getOrElse(defaultSdkVersion) - selectVersion(configuredVersion, supportedVersions) + override final implicit def version: ScalaVersion = { + val configuredOpt = configuredScalaVersion + configuredOpt match { + case Some(exactVersion) => + exactVersion + case None => + val supportedVersions = allTestVersions.filter(supportedIn) + val defaultVersion = defaultVersionOverride.getOrElse(defaultSdkVersion) + val selectedVersion = selectVersion(defaultVersion, supportedVersions) + selectedVersion.orElse( + ScalaVersion.Latest.scalaNext.find(supportedIn) + ).getOrElse(sys.error("Could not find a Scala version matching the test criteria")) + } } private var _injectedScalaVersion: Option[ScalaVersion] = None @@ -51,17 +57,23 @@ trait ScalaSdkOwner extends Test // (including injectedScalaVersion) after test is finished // see HeavyPlatformTestCase.runBare & UsefulTestCase.clearDeclaredFields val listener = - if (reportFailedTestContextDetails) { - val versionsDetailMessage = s"### $buildVersionsDetailsMessage ###" - lazy val logVersion: Unit = System.err.println(versionsDetailMessage) // lazy val to log only once - Some(new TestListener { - override def addError(test: Test, t: Throwable): Unit = logVersion - override def addFailure(test: Test, t: AssertionFailedError): Unit = logVersion - override def endTest(test: Test): Unit = () - override def startTest(test: Test): Unit = () - }) - } - else None + if (reportFailedTestContextDetails) { + var shouldLogVersionFor = ContainerUtil.newConcurrentSet[Test]() + val versionsDetailMessage = s"### $buildVersionsDetailsMessage ###" + + Some(new TestListener { + override def addError(test: Test, t: Throwable): Unit = shouldLogVersionFor.add(test) + override def addFailure(test: Test, t: AssertionFailedError): Unit = shouldLogVersionFor.add(test) + override def startTest(test: Test): Unit = () + override def endTest(test: Test): Unit = { + if (shouldLogVersionFor.contains(test)) { + System.err.println(versionsDetailMessage) + shouldLogVersionFor.remove(test) + } + } + }) + } + else None listener.foreach(result.addListener) super.run(result) @@ -78,19 +90,28 @@ object ScalaSdkOwner { // that should already work in newest version (SCL-15634) val defaultSdkVersion: ScalaVersion = LatestScalaVersions.Scala_2_10 // ScalaVersion.default val preferableSdkVersion: ScalaVersion = LatestScalaVersions.Scala_2_13 - val allTestVersions: SortedSet[ScalaVersion] = { - val allScalaMinorVersions = for { - latestVersion <- LatestScalaVersions.all - minor <- 0 to latestVersion.minorSuffix.toInt - } yield latestVersion.withMinor(minor) - - SortedSet.from(allScalaMinorVersions) + val allTestVersions: SortedSet[ScalaVersion] = SortedSet.from(LatestScalaVersions.all.flatMap(_.generateAllMinorVersions())) + + private def selectVersion(wantedVersion: ScalaVersion, possibleVersions0: SortedSet[ScalaVersion]): Option[ScalaVersion] = { + val possibleVersions = possibleVersions0.iteratorFrom(wantedVersion).toSeq + if (possibleVersions.nonEmpty) { + val first = possibleVersions.head + if (first.isScala3) { + // choose latest possible Scala 3 version + //e.g. `supportedIn >= 3.0.2` -> 3.2.1 + //e.g. `supportedIn == 3.0.2` -> 3.0.2 + Some(possibleVersions.last) + } + else { + //otherwise choose version closes to the "supportedIn" + //e.g. `supportedIn >= 2.12.10` -> 2.12.10 + //TODO: unify this with Scala 3, test failures are expected + Some(first) + } + } + else possibleVersions0.lastOption } - - private def selectVersion(wantedVersion: ScalaVersion, possibleVersions: SortedSet[ScalaVersion]): ScalaVersion = - possibleVersions.iteratorFrom(wantedVersion).nextOption().getOrElse(possibleVersions.last) - lazy val globalConfiguredScalaVersion: Option[ScalaVersion] = { val property = scala.util.Properties.propOrNone("scala.sdk.test.version") .orElse(scala.util.Properties.envOrNone("SCALA_SDK_TEST_VERSION")) diff --git a/src/test/scala/intellij/testfixtures/ScalaVersionProvider.scala b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaVersionProvider.scala similarity index 69% rename from src/test/scala/intellij/testfixtures/ScalaVersionProvider.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/ScalaVersionProvider.scala index 083ad105..b59746fd 100644 --- a/src/test/scala/intellij/testfixtures/ScalaVersionProvider.scala +++ b/src/test/scala/org/jetbrains/plugins/scala/base/ScalaVersionProvider.scala @@ -4,5 +4,6 @@ import org.jetbrains.plugins.scala.ScalaVersion trait ScalaVersionProvider { + //TODO: rename to scalaVersion or something else more meaningful def version: ScalaVersion } diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/SharedTestProjectToken.scala b/src/test/scala/org/jetbrains/plugins/scala/base/SharedTestProjectToken.scala new file mode 100644 index 00000000..4259c6a3 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/SharedTestProjectToken.scala @@ -0,0 +1,26 @@ +package org.jetbrains.plugins.scala.base + +/** + * Test cases with the same Some(token) will share test project if run one by-one.
+ * This can make each test case initialization significantly faster.
+ * If you do not want the project to be shared the token should be None or some other unique value. + * + * @note Suppose test classes A and B use token T1, and test C uses token T2.
+ * If test are run in following order: A, C, B, then project will not be reused between A and B. + * (This is because under the hood IntelliJ platform uses a singleton for storing current test project) + */ +case class SharedTestProjectToken(value: Option[AnyRef]) + +object SharedTestProjectToken { + def apply(value: AnyRef): SharedTestProjectToken = + new SharedTestProjectToken(Some(value)) + + val DoNotShare: SharedTestProjectToken = + SharedTestProjectToken(None) + + def ByScalaSdkAndProjectLibraries(test: LibrariesOwner with ScalaSdkOwner): SharedTestProjectToken = + SharedTestProjectToken((test.version, test.librariesLoadersPublic)) + + def ByTestClassAndScalaSdkAndProjectLibraries(test: LibrariesOwner with ScalaSdkOwner): SharedTestProjectToken = + SharedTestProjectToken((test.getClass, test.version, test.librariesLoadersPublic)) +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/SimpleTestCase.scala b/src/test/scala/org/jetbrains/plugins/scala/base/SimpleTestCase.scala new file mode 100644 index 00000000..4d80f6b5 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/SimpleTestCase.scala @@ -0,0 +1,61 @@ +package org.jetbrains.plugins.scala +package base + +import com.intellij.psi.impl.source.tree.LeafPsiElement +import com.intellij.psi.{PsiComment, PsiElement, PsiWhiteSpace} +import com.intellij.testFramework.fixtures._ +import com.intellij.testFramework.{LightProjectDescriptor, UsefulTestCase} +import org.jetbrains.annotations.Nls +import org.jetbrains.plugins.scala.extensions._ +import org.jetbrains.plugins.scala.lang.psi.api.ScalaFile +import org.jetbrains.plugins.scala.project.ProjectContext +import org.jetbrains.plugins.scala.util.assertions.MatcherAssertions + +abstract class SimpleTestCase extends UsefulTestCase with MatcherAssertions with ScalaCodeParsing { + + var fixture: CodeInsightTestFixture = _ + + implicit def ctx: ProjectContext = fixture.getProject + + override def setUp(): Unit = { + super.setUp() + fixture = createFixture() + fixture.setUp() + } + + protected def createFixture(): CodeInsightTestFixture = { + val factory = IdeaTestFixtureFactory.getFixtureFactory + val builder = factory.createLightFixtureBuilder(getProjectDescriptor, getTestName(false)) + factory.createCodeInsightFixture(builder.getFixture) + } + + protected final def getProjectDescriptor: LightProjectDescriptor = + new ScalaLightProjectDescriptor(sharedProjectToken) + + protected def sharedProjectToken: SharedTestProjectToken = SharedTestProjectToken(this.getClass) + + override def tearDown(): Unit = try { + fixture.tearDown() + } finally { + fixture = null + super.tearDown() + } + + implicit class Findable(private val element: ScalaFile) { + def target: PsiElement = element.depthFirst() + .dropWhile(!_.is[PsiComment]) + .drop(1) + .dropWhile(_.is[PsiWhiteSpace]) + .next() + } + + def describe(tree: PsiElement): String = toString(tree, 0) + + private def toString(root: PsiElement, level: Int): String = { + val indent = List.fill(level)(" ").mkString + val content = if (root.is[LeafPsiElement]) + "\"%s\"".format(root.getText) else root.getClass.getSimpleName + val title = "%s%s\n".format(indent, content) + title + root.children.map(toString(_, level + 1)).mkString + } +} diff --git a/src/test/scala/intellij/testfixtures/IvyManagedLoader.scala b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/IvyManagedLoader.scala similarity index 67% rename from src/test/scala/intellij/testfixtures/IvyManagedLoader.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/IvyManagedLoader.scala index 5634cb70..67f6e88f 100644 --- a/src/test/scala/intellij/testfixtures/IvyManagedLoader.scala +++ b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/IvyManagedLoader.scala @@ -6,6 +6,7 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess import com.intellij.testFramework.PsiTestUtil import org.jetbrains.plugins.scala.DependencyManagerBase.{DependencyDescription, ResolvedDependency} +import org.jetbrains.plugins.scala.util.dependencymanager.TestDependencyManager import scala.collection.mutable @@ -26,15 +27,32 @@ abstract class IvyManagedLoaderBase extends LibraryLoader { } final class IvyManagedLoader private( - override protected val dependencyManager: DependencyManagerBase, - _dependencies: DependencyDescription* - ) extends IvyManagedLoaderBase { + override protected val dependencyManager: DependencyManagerBase, + private val _dependencies: DependencyDescription* +) extends IvyManagedLoaderBase { override protected def cache: mutable.Map[Seq[DependencyDescription], Seq[ResolvedDependency]] = IvyManagedLoader.cache override protected def dependencies(unused: ScalaVersion): Seq[DependencyDescription] = _dependencies + + /** + * NOTE: equals & hashCode are needed for test execution time optimization, + * in order [[org.jetbrains.plugins.scala.base.SharedTestProjectToken.ByTestClassAndScalaSdkAndProjectLibraries]] + * correctly identifies uniqueness of libraries + */ + override def equals(other: Any): Boolean = other match { + case that: IvyManagedLoader => + dependencyManager == that.dependencyManager && + _dependencies == that._dependencies + case _ => false + } + + override def hashCode(): Int = { + val state = Seq(dependencyManager, _dependencies) + state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + } } object IvyManagedLoader { @@ -49,12 +67,4 @@ object IvyManagedLoader { def apply(dependencyManager: DependencyManagerBase, dependencies: DependencyDescription*): IvyManagedLoader = new IvyManagedLoader(dependencyManager, dependencies: _*) -} - -object TestDependencyManager extends DependencyManagerBase { - - // from Michael M.: this blacklist is in order that tested libraries do not transitively fetch `scala-library`, - // which is loaded in a special way in tests via org.jetbrains.plugins.scala.base.libraryLoaders.ScalaSDKLoader - //TODO: should we add scala3-* here? - override val artifactBlackList: Set[String] = Set("scala-library", "scala-reflect", "scala-compiler") -} +} \ No newline at end of file diff --git a/src/test/scala/intellij/testfixtures/LibraryLoader.scala b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/LibraryLoader.scala similarity index 90% rename from src/test/scala/intellij/testfixtures/LibraryLoader.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/LibraryLoader.scala index 47225ca5..ed565c95 100644 --- a/src/test/scala/intellij/testfixtures/LibraryLoader.scala +++ b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/LibraryLoader.scala @@ -4,9 +4,6 @@ package libraryLoaders import com.intellij.openapi.module.Module -/** - * @author adkozlov - */ trait LibraryLoader { def init(implicit module: Module, version: ScalaVersion): Unit diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/MockScalaSDKLoader.scala b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/MockScalaSDKLoader.scala new file mode 100644 index 00000000..3c04b207 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/MockScalaSDKLoader.scala @@ -0,0 +1,46 @@ +package org.jetbrains.plugins.scala.base.libraryLoaders + +import com.intellij.openapi.module.Module +import com.intellij.openapi.roots.libraries.{Library, LibraryTablesRegistrar} +import com.intellij.openapi.roots.ui.configuration.libraryEditor.ExistingLibraryEditor +import com.intellij.testFramework.PsiTestUtil +import org.jetbrains.plugins.scala.ScalaVersion +import org.jetbrains.plugins.scala.extensions.{ObjectExt, inWriteAction} +import org.jetbrains.plugins.scala.project.{ModuleExt, ScalaLibraryProperties, ScalaLibraryType} + +import java.{util => ju} + +/** + * This loader creates a lightweight scala sdk. + * It can be useful when you just want to tell that a module has Scala SDK with some version + * (e.g. in order Scala 3 files are properly parsed) + * and when you do not need everything else (compiler classpath, sources) + */ +final class MockScalaSDKLoader() extends LibraryLoader { + + override def init(implicit module: Module, version: ScalaVersion): Unit = { + val libraryTable = LibraryTablesRegistrar.getInstance.getLibraryTable(module.getProject) + val scalaSdkName = s"mock-scala-sdk-${version.minor}" + + def createNewLibrary: Library = + PsiTestUtil.addProjectLibrary(module, scalaSdkName, ju.List.of(), ju.List.of()) + + val library = + libraryTable.getLibraryByName(scalaSdkName) + .toOption + .getOrElse(createNewLibrary) + + inWriteAction { + val properties = ScalaLibraryProperties(Some(version.minor), Seq.empty, Seq.empty) + + val editor = new ExistingLibraryEditor(library, null) + editor.setType(ScalaLibraryType()) + editor.setProperties(properties) + editor.commit() + + val model = module.modifiableModel + model.addLibraryEntry(library) + model.commit() + } + } +} \ No newline at end of file diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaLibraryLoader.scala b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaLibraryLoader.scala new file mode 100644 index 00000000..0874bfdf --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaLibraryLoader.scala @@ -0,0 +1,90 @@ +package org.jetbrains.plugins.scala.base.libraryLoaders + +import com.intellij.openapi.module.Module +import com.intellij.openapi.roots.libraries.{Library, LibraryTablesRegistrar} +import com.intellij.openapi.vfs.{JarFileSystem, VirtualFile} +import com.intellij.testFramework.PsiTestUtil +import org.jetbrains.plugins.scala.DependencyManagerBase.Resolver +import org.jetbrains.plugins.scala.extensions.ObjectExt +import org.jetbrains.plugins.scala.{DependencyManager, DependencyManagerBase, ScalaVersion} + +import java.io.File +import java.{util => ju} + +/** + * The loader loads and registers only nscala library (with sources) without transitive dependencies + * It doesn't load compiler classpath jars and creates a simple library + */ +final case class ScalaLibraryLoader( + scalaVersion: ScalaVersion, + dependencyManager: DependencyManagerBase = DependencyManager +) + extends LibraryLoader { + + import DependencyManagerBase._ + import ScalaLibraryLoader.findJarFile + + //NOTE: we ignore implicitly passed ScalaVersion and use version explicitly set in the parameters + override def init(implicit module: Module, ignored: ScalaVersion): Unit = { + initImpl(module) + } + + private def initImpl(module: Module): Unit = { + import scala.jdk.CollectionConverters._ + + implicit val scalaVersionImplicit: ScalaVersion = scalaVersion + + val scalaLibraryClasses: ju.List[VirtualFile] = { + val files: Seq[File] = dependencyManager.resolve(scalaLibraryDescription).map(_.file) + files.map(findJarFile).asJava + } + val scalaLibrarySources: ju.List[VirtualFile] = { + val files = dependencyManager.resolve(scalaLibraryDescription % Types.SRC).map(_.file) + files.map(findJarFile).asJava + } + + val libraryTable = LibraryTablesRegistrar.getInstance.getLibraryTable(module.getProject) + val scalaLibraryName = s"scala-library-${scalaVersion.minor}" + + def createNewLibrary: Library = + PsiTestUtil.addProjectLibrary( + module, + scalaLibraryName, + scalaLibraryClasses, + scalaLibrarySources + ) + + val existingLibrary = Option(libraryTable.getLibraryByName(scalaLibraryName)) + existingLibrary.getOrElse(createNewLibrary) + } +} + +object ScalaLibraryLoader { + + private def findJarFile(file: File) = + JarFileSystem.getInstance().refreshAndFindFileByPath { + file.getCanonicalPath + "!/" + } + + /** + * This utility "overrides" default scala sdk loader. use non-standard resolvers + * It uses separate scala libraries with specified versions + */ + def libraryLoadersWithSeparateScalaLibraries( + superLibraryLoaders: Seq[LibraryLoader], + scala2Version: ScalaVersion, + scala3Version: ScalaVersion, + ): Seq[LibraryLoader] = { + val scala2LibraryLoader = ScalaLibraryLoader(scala2Version) + val scala3LibraryLoader = ScalaLibraryLoader(scala3Version) + + //We use resolveScalaLibraryTransitiveDependencies = false in order to use the latest 2.13.14 RC version + val scala3SdkLoader = ScalaSDKLoader(includeLibraryFilesInSdk = false) + + Seq( + scala3LibraryLoader, + scala2LibraryLoader, + scala3SdkLoader + ) ++ superLibraryLoaders.filterNot(_.is[ScalaSDKLoader]) + } +} \ No newline at end of file diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaReflectLibraryLoader.scala b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaReflectLibraryLoader.scala new file mode 100644 index 00000000..c240ee6d --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaReflectLibraryLoader.scala @@ -0,0 +1,20 @@ +package org.jetbrains.plugins.scala.base.libraryLoaders + +import org.jetbrains.plugins.scala.DependencyManagerBase.{DependencyDescription, ResolvedDependency, RichStr} +import org.jetbrains.plugins.scala.{DependencyManager, DependencyManagerBase, ScalaVersion} + +import scala.collection.mutable + +object ScalaReflectLibraryLoader extends IvyManagedLoaderBase { + override protected def dependencyManager: DependencyManagerBase = DependencyManager + + override protected def dependencies(scalaVersion: ScalaVersion): Seq[DependencyManagerBase.DependencyDescription] = + Seq( + "org.scala-lang" % "scala-reflect" % scalaVersion.minor + ) + + override protected val cache: mutable.Map[ + Seq[DependencyDescription], + Seq[ResolvedDependency] + ] = mutable.Map() +} \ No newline at end of file diff --git a/src/test/scala/intellij/testfixtures/ScalaSDKLoader.scala b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaSDKLoader.scala similarity index 55% rename from src/test/scala/intellij/testfixtures/ScalaSDKLoader.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaSDKLoader.scala index 98b5692e..bc1ddd05 100644 --- a/src/test/scala/intellij/testfixtures/ScalaSDKLoader.scala +++ b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ScalaSDKLoader.scala @@ -1,6 +1,4 @@ -package org.jetbrains.plugins.scala -package base -package libraryLoaders +package org.jetbrains.plugins.scala.base.libraryLoaders import com.intellij.openapi.module.Module import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar @@ -8,35 +6,49 @@ import com.intellij.openapi.roots.ui.configuration.libraryEditor.ExistingLibrary import com.intellij.openapi.vfs.{JarFileSystem, VirtualFile} import com.intellij.testFramework.PsiTestUtil import org.jetbrains.plugins.scala.extensions.{ObjectExt, inWriteAction} -import org.jetbrains.plugins.scala.project.{ModuleExt, ScalaLanguageLevel, ScalaLibraryProperties, ScalaLibraryType, template} +import org.jetbrains.plugins.scala.project.external.ScalaSdkUtils +import org.jetbrains.plugins.scala.project.{ModuleExt, ScalaLibraryProperties, ScalaLibraryType, template} +import org.jetbrains.plugins.scala.{DependencyManager, DependencyManagerBase, ScalaVersion} import org.junit.Assert._ import java.io.File import java.{util => ju} -case class ScalaSDKLoader(includeScalaReflect: Boolean = false, includeScalaCompiler: Boolean = false) extends LibraryLoader { - - protected lazy val dependencyManager: DependencyManagerBase = DependencyManager +/** @param includeScalaReflectIntoCompilerClasspath also see [[ScalaReflectLibraryLoader]] */ +case class ScalaSDKLoader( + includeScalaReflectIntoCompilerClasspath: Boolean = false, + //TODO: drop this parameter and fix tests + includeScalaCompilerIntoLibraryClasspath: Boolean = false, + includeLibraryFilesInSdk: Boolean = true, + compilerBridgeBinaryJar: Option[File] = None, + dependencyManager: DependencyManagerBase = DependencyManager +) extends LibraryLoader { import DependencyManagerBase._ import ScalaSDKLoader._ import template.Artifact + def withResolvers(_resolvers: Seq[Resolver]): ScalaSDKLoader = { + val dependencyManager = new DependencyManagerBase { + override protected def resolvers: Seq[Resolver] = _resolvers + } + copy(dependencyManager = dependencyManager) + } + protected def binaryDependencies(implicit version: ScalaVersion): List[DependencyDescription] = - version.languageLevel match { // TODO maybe refactoring? - case ScalaLanguageLevel.Scala_3_0 => - List( - scalaCompilerDescription.transitive(), - scalaLibraryDescription.transitive(), - DependencyDescription("org.scala-lang", "scala3-interfaces", version.minor), - ) - - case _ => - val maybeScalaReflect = if (includeScalaReflect) Some(scalaReflectDescription) else None - List( - scalaCompilerDescription, - scalaLibraryDescription - ) ++ maybeScalaReflect + if (version.languageLevel.isScala3) { + List( + scalaCompilerDescription.transitive(), + scalaLibraryDescription.transitive(), + DependencyDescription("org.scala-lang", "scala3-interfaces", version.minor), + ) + } + else { + val maybeScalaReflect = if (includeScalaReflectIntoCompilerClasspath) Some(scalaReflectDescription) else None + List( + scalaCompilerDescription, + scalaLibraryDescription + ) ++ maybeScalaReflect } protected def sourcesDependency(implicit version: ScalaVersion): DependencyDescription = @@ -51,7 +63,7 @@ case class ScalaSDKLoader(includeScalaReflect: Boolean = false, includeScalaComp val dependencies = binaryDependencies val resolved = dependencyManager.resolve(dependencies: _*) - if (version.languageLevel == ScalaLanguageLevel.Scala_3_0) + if (version.isScala3) assertTrue( s"Failed to resolve scala sdk version $version, result:\n${resolved.mkString("\n")}", resolved.size >= dependencies.size @@ -66,6 +78,9 @@ case class ScalaSDKLoader(includeScalaReflect: Boolean = false, includeScalaComp val (resolvedOk, resolvedMissing) = resolved.partition(_.file.exists()) val compilerClasspath = resolvedOk.map(_.file) + // Manually resolve a compiler bridge only if it hasn't been provided. This allows testing with a custom bridge. + val compilerBridge = compilerBridgeBinaryJar.orElse(ScalaSdkUtils.resolveCompilerBridgeJar(version.minor)) + assertTrue( s"Some SDK jars were resolved but for some reason do not exist:\n$resolvedMissing", resolvedMissing.isEmpty @@ -79,23 +94,31 @@ case class ScalaSDKLoader(includeScalaReflect: Boolean = false, includeScalaComp fail(s"Local SDK files should contain compiler jar for : $version\n${compilerClasspath.mkString("\n")}").asInstanceOf[Nothing] } - val classesRoots = { - import scala.jdk.CollectionConverters._ - val files = - if (includeScalaCompiler) compilerClasspath - else compilerClasspath.filterNot(compilerFile == _) - files.map(findJarFile).asJava - } + val scalaLibraryClasses: Seq[VirtualFile] = + if (includeLibraryFilesInSdk) { + val files = + if (includeScalaCompilerIntoLibraryClasspath) compilerClasspath + else compilerClasspath.filter(_.getName.matches(".*(scala-library|scala3-library).*")) + files.map(findJarFile) + } + else Nil + + val scalaLibrarySources = + if (includeLibraryFilesInSdk) Seq(sourceRoot) + else Nil val libraryTable = LibraryTablesRegistrar.getInstance.getLibraryTable(module.getProject) val scalaSdkName = s"scala-sdk-${version.minor}" - def createNewLibrary = PsiTestUtil.addProjectLibrary( - module, - scalaSdkName, - classesRoots, - ju.Collections.singletonList(sourceRoot) - ) + import scala.jdk.CollectionConverters._ + + def createNewLibrary = + PsiTestUtil.addProjectLibrary( + module, + scalaSdkName, + scalaLibraryClasses.asJava, + scalaLibrarySources.asJava + ) val library = libraryTable.getLibraryByName(scalaSdkName) @@ -104,7 +127,7 @@ case class ScalaSDKLoader(includeScalaReflect: Boolean = false, includeScalaComp inWriteAction { val version = Artifact.ScalaCompiler.versionOf(compilerFile) - val properties = ScalaLibraryProperties(version, compilerClasspath, Seq.empty) + val properties = ScalaLibraryProperties(version, compilerClasspath, Seq.empty, compilerBridge) val editor = new ExistingLibraryEditor(library, null) editor.setType(ScalaLibraryType()) @@ -124,4 +147,4 @@ object ScalaSDKLoader { JarFileSystem.getInstance().refreshAndFindFileByPath { file.getCanonicalPath + "!/" } -} +} \ No newline at end of file diff --git a/src/test/scala/intellij/testfixtures/SmartJDKLoader.scala b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/SmartJDKLoader.scala similarity index 88% rename from src/test/scala/intellij/testfixtures/SmartJDKLoader.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/SmartJDKLoader.scala index 3d8c3ffa..bff61578 100644 --- a/src/test/scala/intellij/testfixtures/SmartJDKLoader.scala +++ b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/SmartJDKLoader.scala @@ -7,9 +7,11 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl import com.intellij.openapi.projectRoots.{JavaSdk, JavaSdkVersion, Sdk} import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess import com.intellij.pom.java.LanguageLevel import com.intellij.testFramework.IdeaTestUtil +import com.intellij.util.SystemProperties import org.jetbrains.plugins.scala.extensions.inWriteAction import org.junit.Assert @@ -24,8 +26,8 @@ case class InternalJDKLoader() extends SmartJDKLoader() { } /** - * Consider using this instead of HeavyJDKLoader if you don't need java interop in your tests - */ + * Consider using this instead of HeavyJDKLoader if you don't need java interop in your tests + */ case class MockJDKLoader(languageLevel: LanguageLevel = LanguageLevel.JDK_17) extends SmartJDKLoader() { override protected def createSdkInstance(): Sdk = IdeaTestUtil.getMockJdk(languageLevel.toJavaVersion) } @@ -35,15 +37,16 @@ case class HeavyJDKLoader(languageLevel: LanguageLevel = LanguageLevel.JDK_17) e } abstract class SmartJDKLoader() extends LibraryLoader { + private lazy val instance: Sdk = createSdkInstance() + override def init(implicit module: Module, version: ScalaVersion): Unit = { - ModuleRootModificationUtil.setModuleSdk(module, createSdkInstance()) + ModuleRootModificationUtil.setModuleSdk(module, instance) } override def clean(implicit module: Module): Unit = { ModuleRootModificationUtil.setModuleSdk(module, null) val jdkTable = JavaAwareProjectJdkTableImpl.getInstanceEx - val allJdks = jdkTable.getAllJdks - inWriteAction { allJdks.foreach(jdkTable.removeJdk) } + inWriteAction(jdkTable.removeJdk(instance)) } protected def createSdkInstance(): Sdk @@ -52,18 +55,20 @@ abstract class SmartJDKLoader() extends LibraryLoader { object SmartJDKLoader { private val jdkPaths = { - val userHome = System.getProperty("user.home") + val userHome = SystemProperties.getUserHome Seq( "/usr/lib/jvm", // linux style "C:\\Program Files\\Java\\", // windows style "C:\\Program Files (x86)\\Java\\", // windows 32bit style "/Library/Java/JavaVirtualMachines", // mac style + userHome + "/Downloads/jdk", // mac style userHome + "/Library/Java/JavaVirtualMachines", // mac style userHome + "/.jabba/jdk", // jabba (for github actions) userHome + "/.jdks", // by default IDEA downloads JDKs here + userHome + "/.sdkman/candidates/java" // SDKMAN style ) } - + def getOrCreateJDK(languageLevel: LanguageLevel = LanguageLevel.JDK_17): Sdk = { val jdkVersion = JavaSdkVersion.fromLanguageLevel(languageLevel) val jdkName = jdkVersion.getDescription @@ -95,7 +100,7 @@ object SmartJDKLoader { val versionStrings = Seq(s"1.$versionMajor", s"-$versionMajor", s"jdk$versionMajor") val fromEnv = sys.env.get(jdkVersion.toString).orElse(sys.env.get(s"${jdkVersion}_0")) val fromEnv64 = sys.env.get(s"${jdkVersion}_x64").orElse(sys.env.get(s"${jdkVersion}_0_x64")) // teamcity style - val priorityPaths = Seq(currentJava(versionMajor), fromEnv.orElse(fromEnv64).map(new File(_))).flatten + val priorityPaths = Seq(currentJava(versionMajor), fromEnv.orElse(fromEnv64)).flatten.map(new File(_)) priorityPaths.headOption .orElse { @@ -116,7 +121,7 @@ object SmartJDKLoader { b.getName == "bin" && b.listFiles().exists(x => x.getName == "javac.exe" || x.getName == "javac") } - } + }.orElse(Some(dir)) } private def inJvm(path: String, versionString: String): List[File] = @@ -132,8 +137,9 @@ object SmartJDKLoader { } private def currentJava(versionMajor: String) = - sys.props.get("java.version") + Some(SystemInfo.JAVA_VERSION) .filter(v => v.startsWith(s"1.$versionMajor") || v.startsWith(versionMajor)) .flatMap(_ => sys.props.get("java.home")) - .flatMap(d => findJDK(new File(d).getParentFile)) -} \ No newline at end of file +} + + diff --git a/src/test/scala/intellij/testfixtures/SourcesLoader.scala b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/SourcesLoader.scala similarity index 100% rename from src/test/scala/intellij/testfixtures/SourcesLoader.scala rename to src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/SourcesLoader.scala diff --git a/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ThirdPartyLibraryLoader.scala b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ThirdPartyLibraryLoader.scala new file mode 100644 index 00000000..94027acf --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/base/libraryLoaders/ThirdPartyLibraryLoader.scala @@ -0,0 +1,30 @@ +package org.jetbrains.plugins.scala +package base +package libraryLoaders + +import com.intellij.openapi.module.Module +import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess +import com.intellij.testFramework.PsiTestUtil +import org.jetbrains.plugins.scala.project.ModuleExt + +import java.io.File + +trait ThirdPartyLibraryLoader extends LibraryLoader { + protected val name: String + + override def init(implicit module: Module, version: ScalaVersion): Unit = { + val alreadyExistsInModule = + module.libraries.map(_.getName) + .contains(name) + + if (alreadyExistsInModule) return + + val path = this.path + val file = new File(path).getCanonicalFile + assert(file.exists(), s"library root for $name does not exist at $file") + VfsRootAccess.allowRootAccess(module, path) + PsiTestUtil.addLibrary(module, name, file.getParent, file.getName) + } + + protected def path(implicit version: ScalaVersion): String +} \ No newline at end of file diff --git a/src/test/scala/intellij/testfixtures/testCategories.scala b/src/test/scala/org/jetbrains/plugins/scala/testCategories.scala similarity index 89% rename from src/test/scala/intellij/testfixtures/testCategories.scala rename to src/test/scala/org/jetbrains/plugins/scala/testCategories.scala index 03f6e9dc..286660cc 100644 --- a/src/test/scala/intellij/testfixtures/testCategories.scala +++ b/src/test/scala/org/jetbrains/plugins/scala/testCategories.scala @@ -25,7 +25,7 @@ trait WorksheetEvaluationTests trait RandomTypingTests trait HighlightingTests -/** Tests that may fail intermittently or depending on environment. +/** Tests that may fail intermittently or depending on environment. * Eg run locally but not on build server. */ trait FlakyTests @@ -42,3 +42,10 @@ trait CompletionTests * `org.jetbrains.plugins.scala.codeInsight.InlayHintsTestBase`. */ trait EditorTests + +trait CompilationTests + +/** + * See [[org.jetbrains.plugins.scala.internal.bundle.ScalaBundleSortingTest]] + */ +trait BundleSortingTests diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/AliasExports.scala b/src/test/scala/org/jetbrains/plugins/scala/util/AliasExports.scala new file mode 100644 index 00000000..5d9df026 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/AliasExports.scala @@ -0,0 +1,17 @@ +package org.jetbrains.plugins.scala.util + +import org.jetbrains.plugins.scala.project.ProjectContext +import org.jetbrains.plugins.scala.settings.ScalaProjectSettings + +object AliasExports { + def aliasExportsEnabled(implicit context: ProjectContext): Boolean = + ScalaProjectSettings.in(context.project).aliasExportsEnabled + + def stringClass(implicit context: ProjectContext): String = + if (aliasExportsEnabled) "java.lang.String" + else "scala.Predef.String" + + def exceptionClass(implicit context: ProjectContext): String = + if (aliasExportsEnabled) "java.lang.Exception" + else "scala.Exception" +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/BitMaskTest.scala b/src/test/scala/org/jetbrains/plugins/scala/util/BitMaskTest.scala new file mode 100644 index 00000000..53b44e46 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/BitMaskTest.scala @@ -0,0 +1,99 @@ +package org.jetbrains.plugins.scala.util + +import junit.framework.TestCase +import org.jetbrains.plugins.scala.project.ScalaLanguageLevel +import org.jetbrains.plugins.scala.util.assertions.AssertionMatchers + +import scala.collection.compat.immutable.ArraySeq +import scala.util.Random + +class BitMaskTest extends TestCase with AssertionMatchers { + + //noinspection TypeAnnotation + object ExampleMask extends BitMaskStorage { + val b1 = bool("b1") + val n1 = nat(max = 5, "n1") + val b2 = bool("b2") + val i1 = int(min = -4, max = 22, "i1") + val l1 = jEnum[ScalaLanguageLevel]("l1") + + override val version: Int = finishAndMakeVersion() + } + + def testExampleMask(): Unit = { + import ExampleMask._ + + b1.pos shouldBe 0 + b1.chunkSize shouldBe 1 + + n1.pos shouldBe 1 + n1.chunkSize shouldBe 3 + + b2.pos shouldBe 4 + b2.chunkSize shouldBe 1 + + i1.pos shouldBe 5 + i1.chunkSize shouldBe 5 + i1.shiftedMax shouldBe 26 + + l1.pos shouldBe 10 + } + + def testRandom(): Unit = { + val rand = new Random(123) + + def makeRandomMaskStorage(): BitMaskStorage = new BitMaskStorage { + for (i <- 0 to rand.nextInt(8)) { + try rand.nextInt(3) match { + case 0 => bool(s"b$i") + case 1 => nat(max = math.abs(rand.nextInt()) min 1, s"n$i") + case 2 => + val Seq(a, b) = Seq(rand.nextInt(), rand.nextInt()).sorted + int(a min b, a max b, s"i$i") + case _ => ??? + } + catch { + case a: AssertionError if a.getMessage.contains("Do not have space") => + // cheap hack so we don't have to test if there is still space left + } + } + + override val version: Int = finishAndMakeVersion() + } + + def makeRandomValueFor(mask: BitMask): mask.T = { + val v = mask match { + case BitMask.Bool(_) => rand.nextBoolean() + case BitMask.Nat(_, max) => rand.nextInt(max + 1) + case BitMask.Integer(_, min, max) => rand.between(min, max + 1) + case _ => ??? + } + v.asInstanceOf[mask.T] + } + + for (_ <- 0 to 10000) { + val storage = makeRandomMaskStorage() + val masks = storage.members.values.to(ArraySeq) + var current = 0 + + for (_ <- 0 to 1000) { + val oldValues = masks.map(_.read(current)) + + val i = rand.nextInt(masks.length) + val mask = masks(i) + val newValue = makeRandomValueFor(mask) + + val newCurrent = mask.write(current, newValue) + + // test if write was correct + mask.read(newCurrent) shouldBe newValue + + // test if other values are still the same + val newValues = masks.map(_.read(newCurrent)) + newValues.patch(i, Nil, 1) shouldBe oldValues.patch(i, Nil, 1) + + current = newCurrent + } + } + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/CompilerTestUtil.scala b/src/test/scala/org/jetbrains/plugins/scala/util/CompilerTestUtil.scala new file mode 100644 index 00000000..82b082b0 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/CompilerTestUtil.scala @@ -0,0 +1,92 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.registry.Registry +import com.intellij.util.xmlb.XmlSerializerUtil +import org.jetbrains.plugins.scala.settings.{ScalaCompileServerSettings, ScalaHighlightingMode, ScalaProjectSettings} + +import scala.util.Try + +object CompilerTestUtil { + + private def compileServerSettings: ScalaCompileServerSettings = + ScalaCompileServerSettings.getInstance().ensuring( + _ != null, + "could not get instance of compileServerSettings. Was plugin artifact built before running test?" + ) + + def withModifiedCompileServerSettings(body: ScalaCompileServerSettings => Unit): RevertableChange = new RevertableChange { + private var settingsBefore: ScalaCompileServerSettings = _ + private lazy val settings: ScalaCompileServerSettings = compileServerSettings + + override def applyChange(): Unit = { + settingsBefore = XmlSerializerUtil.createCopy(settings) + body(settings) + com.intellij.compiler.CompilerTestUtil.saveApplicationComponent(settings) + } + + override def revertChange(): Unit = { + XmlSerializerUtil.copyBean(settingsBefore, settings) + com.intellij.compiler.CompilerTestUtil.saveApplicationComponent(settings) + } + } + + def withEnabledCompileServer(enable: Boolean): RevertableChange = withModifiedCompileServerSettings { settings => + settings.COMPILE_SERVER_ENABLED = enable + settings.COMPILE_SERVER_SHUTDOWN_IDLE = true + settings.COMPILE_SERVER_SHUTDOWN_DELAY = 30 + } + + def withForcedJdkForBuildProcess(jdk: Sdk): RevertableChange = new RevertableChange { + private var jdkBefore: Option[String] = None + + override def applyChange(): Unit = { + jdk.getHomeDirectory match { + case null => + throw new RuntimeException(s"Failed to set up JDK, got: $jdk") + case homeDirectory => + val jdkHome = homeDirectory.getCanonicalPath + //see com.intellij.compiler.server.BuildManager.COMPILER_PROCESS_JDK_PROPERTY + val registry = Registry.get("compiler.process.jdk") + jdkBefore = Try(registry.asString).toOption + registry.setValue(jdkHome) + } + } + + override def revertChange(): Unit = + jdkBefore.foreach { jdk => + Registry.get("compiler.process.jdk").setValue(jdk) + } + } + + def withCompileServerJdk(sdk: Sdk): RevertableChange = + withModifiedCompileServerSettings { settings => + settings.USE_DEFAULT_SDK = false + settings.COMPILE_SERVER_SDK = sdk.getName + } + + private def withErrorsFromCompiler(project: Project, enabled: Boolean): RevertableChange = { + val revertible1 = RevertableChange.withModifiedSetting( + ScalaProjectSettings.getInstance(project).isCompilerHighlightingScala2, + ScalaProjectSettings.getInstance(project).setCompilerHighlightingScala2(_), + enabled + ) + val revertible2 = RevertableChange.withModifiedSetting( + ScalaProjectSettings.getInstance(project).isCompilerHighlightingScala3, + ScalaProjectSettings.getInstance(project).setCompilerHighlightingScala3(_), + enabled + ) + val revertible3 = RevertableChange.withModifiedSetting[Boolean]( + ScalaHighlightingMode.compilerHighlightingEnabledInTests, + ScalaHighlightingMode.compilerHighlightingEnabledInTests = _, + enabled + ) + revertible1 |+| revertible2 |+| revertible3 + } + + def runWithErrorsFromCompiler(project: Project)(body: => Unit): Unit = { + val revertable: RevertableChange = withErrorsFromCompiler(project, enabled = true) + revertable.run(body) + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/ConfigureJavaFile.scala b/src/test/scala/org/jetbrains/plugins/scala/util/ConfigureJavaFile.scala new file mode 100644 index 00000000..4a836d03 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/ConfigureJavaFile.scala @@ -0,0 +1,24 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.testFramework.LightPlatformTestCase +import org.jetbrains.plugins.scala.extensions.inWriteAction + +object ConfigureJavaFile { + def configureJavaFile(fileText: String, + className: String, + packageName: String = null): Unit = inWriteAction { + val root = LightPlatformTestCase.getSourceRoot match { + case sourceRoot if packageName == null => sourceRoot + case sourceRoot => sourceRoot.createChildDirectory(null, packageName) + } + + val file = root.createChildData(null, className + ".java") + VfsUtil.saveText(file, normalize(fileText)) + } + + private def normalize(text: String, trimContent: Boolean = true): String = { + val result = text.stripMargin.replace("\r", "") + if (trimContent) result.trim else result + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/EditorHintFixtureEx.scala b/src/test/scala/org/jetbrains/plugins/scala/util/EditorHintFixtureEx.scala new file mode 100644 index 00000000..e180c0c0 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/EditorHintFixtureEx.scala @@ -0,0 +1,26 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.openapi.Disposable +import com.intellij.testFramework.fixtures.EditorHintFixture + +class EditorHintFixtureEx(parentDisposable: Disposable) extends EditorHintFixture(parentDisposable) { + + /** + * The whole hint text can have a lot of boilerplate HTML code, + * added by [[com.intellij.codeInsight.hint.HintUtil.createInformationLabel]] which is used when showing the hint test. + * During the tests we are mostly interested in the generated body text. + */ + def getCurrentHintBodyText: String = { + val text = super.getCurrentHintText + + val BodyStartTag = "" + val BodyEndTag = "" + + val bodyStart = text.indexOf(BodyStartTag) + val bodyEnd = text.indexOf(BodyEndTag, bodyStart) + if (bodyStart >= 0 || bodyEnd >= 0) + text.substring(bodyStart + BodyStartTag.length, bodyEnd) + else + throw new AssertionError(s"Can't find html content in text:\n$text") + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/EnumSetTest.scala b/src/test/scala/org/jetbrains/plugins/scala/util/EnumSetTest.scala new file mode 100644 index 00000000..e8f3b192 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/EnumSetTest.scala @@ -0,0 +1,44 @@ +package org.jetbrains.plugins.scala.util + +import junit.framework.TestCase +import org.jetbrains.plugins.scala.util.EnumSet._ +import org.junit.Assert._ + +class EnumSetTest extends TestCase { + import JavaEnum._ + + def testEnumSet(): Unit = { + + val ab = EnumSet(A, B, B, B, B, A, B) + val ab2 = EnumSet.empty[JavaEnum] ++ A ++ B + + assertTrue(ab.contains(A) && ab.contains(B) && !ab.contains(C) && !ab.contains(D)) + assertTrue(ab2.contains(A) && ab2.contains(B) && !ab2.contains(C) && !ab2.contains(D)) + + assertTrue(ab == ab2) + + val bc = EnumSet(B) ++ EnumSet(C) + + assertTrue(!bc.contains(A) && bc.contains(B) && bc.contains(C) && !bc.contains(D)) + + val abc = ab ++ bc + + assertTrue(abc.contains(A) && abc.contains(B) && abc.contains(C) && !abc.contains(D)) + } + + def testEnumSetToArray(): Unit = { + val acd = EnumSet(D) ++ EnumSet(A) ++ C + + assertTrue(acd.toArray sameElements Array(A, C, D)) + } + + def testEnumSetIsEmpty(): Unit = { + val empty1 = EnumSet.empty[JavaEnum] + val empty2 = EnumSet[JavaEnum]() + val empty3 = EnumSet.readFromInt[JavaEnum](0) + + assertTrue(empty1.isEmpty && empty2.isEmpty && empty3.isEmpty) + assertTrue(empty1 == empty2 && empty2 == empty3) + } + +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/FindCaretOffset.scala b/src/test/scala/org/jetbrains/plugins/scala/util/FindCaretOffset.scala new file mode 100644 index 00000000..3c36ee6b --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/FindCaretOffset.scala @@ -0,0 +1,38 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.testFramework.EditorTestUtil.CARET_TAG +import org.jetbrains.plugins.scala.extensions.StringExt +import org.junit.Assert.fail + +object FindCaretOffset { + def findCaretOffset(text: String, stripTrailingSpaces: Boolean): (String, Int) = { + val (textActual, caretOffsets) = findCaretOffsets(text, stripTrailingSpaces) + caretOffsets match { + case Seq(caretIdx) => (textActual, caretIdx) + case Seq() => (textActual, -1) + case _ => fail(s"single caret expected but found: ${caretOffsets.size}").asInstanceOf[Nothing] + } + } + + def findCaretOffsets(text: String, trimText: Boolean): (String, Seq[Int]) = { + + val textNormalized = if (trimText) text.withNormalizedSeparator.trim else text.withNormalizedSeparator + + def caretIndex(offset: Int) = textNormalized.indexOf(CARET_TAG, offset) + + @scala.annotation.tailrec + def collectCaretIndices(idx: Int)(indices: Seq[Int]): Seq[Int] = + if (idx < 0) indices else { + val nextIdx = caretIndex(idx + 1) + collectCaretIndices(nextIdx)(indices :+ idx) + } + + val caretIndices = collectCaretIndices(caretIndex(0))(Seq[Int]()) + val caretIndicesNormalized = caretIndices.zipWithIndex.map { case (caretIdx, idx) => caretIdx - idx * CARET_TAG.length } + ( + textNormalized.replace(CARET_TAG, ""), + caretIndicesNormalized + ) + } + +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/GeneratedTestSuiteFactory.scala b/src/test/scala/org/jetbrains/plugins/scala/util/GeneratedTestSuiteFactory.scala new file mode 100644 index 00000000..7cc11ae4 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/GeneratedTestSuiteFactory.scala @@ -0,0 +1,113 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.util.ThrowableRunnable +import junit.framework.{Test, TestSuite} +import org.jetbrains.plugins.scala.ScalaVersion +import org.jetbrains.plugins.scala.base.{ScalaLightCodeInsightFixtureTestCase, SharedTestProjectToken, SimpleTestCase} +import org.jetbrains.plugins.scala.extensions.BooleanExt +import org.jetbrains.plugins.scala.util.GeneratedTestSuiteFactory.{SimpleTestData, SingleCodeTestData} +import org.jetbrains.plugins.scala.util.assertions.AssertionMatchers +import org.junit.Ignore + + +abstract class GeneratedTestSuiteFactory { + type TestData = GeneratedTestSuiteFactory.TestData + type TD <: TestData + + def testData: Seq[TD] + def makeActualTest(testData: TD): Test + + final def suite: TestSuite = { + val testSuite = new TestSuite() + testData.map(makeActualTest).foreach(testSuite.addTest) + testSuite + } + + protected final def testDataFromCode(code: String): SimpleTestData = SimpleTestData.fromCode(code) + + //noinspection JUnitMalformedDeclaration + @Ignore + protected class SimpleHighlightingActualTest(testData: SingleCodeTestData, minScalaVersion: ScalaVersion) extends ScalaLightCodeInsightFixtureTestCase with AssertionMatchers { + this.setName(testData.testName) + + override protected def sharedProjectToken: SharedTestProjectToken = SharedTestProjectToken(GeneratedTestSuiteFactory) + override protected def supportedIn(version: ScalaVersion): Boolean = version >= minScalaVersion + + override def runTestRunnable(testRunnable: ThrowableRunnable[Throwable]): Unit = { + checkTextHasNoErrors(testData.testCode) + } + + override protected def shouldPass: Boolean = !testData.isFailing + } + + //noinspection JUnitMalformedDeclaration + @Ignore + protected abstract class SimpleActualTest(testData: TestData, minScalaVersion: ScalaVersion) extends SimpleTestCase with AssertionMatchers { + this.setName(testData.testName) + + override protected def scalaVersion: ScalaVersion = ScalaVersion.Latest.Scala_3 + + override def runTestRunnable(testRunnable: ThrowableRunnable[Throwable]): Unit = runActualTest() + def runActualTest(): Unit + + override protected def shouldPass: Boolean = !testData.isFailing + } + +} + +object GeneratedTestSuiteFactory { + trait TestData { + def testName: String + def checkCodeFragment: String + def failureExpectation: Option[FailureExpectation] = None + + final def isFailing: Boolean = failureExpectation.nonEmpty + } + + sealed case class FailureExpectation(errors: Seq[TestDataError])(val linesCovered: Boolean, val messagesCovered: Boolean) { + assert(!linesCovered || errors.forall(_.line.nonEmpty)) + assert(!messagesCovered || errors.forall(_.message.nonEmpty)) + } + object FailureExpectation { + def fromErrors(errors: Seq[TestDataError], linesCovered: Boolean = false, messagesCovered: Boolean = false): Option[FailureExpectation] = + errors.nonEmpty.option(FailureExpectation(errors)(linesCovered, messagesCovered)) + } + + case class TestDataError(line: Option[Int], message: Option[TestDataErrorMessage]) { + assert(line.nonEmpty || message.nonEmpty) + } + + trait SingleCodeTestData extends TestData { + def testCode: String + } + + final case class TestDataErrorMessage(scalaPluginMessage: String, scalaCompilerMessage: String) + + final case class SimpleTestData(override val testName: String, + override val testCode: String, + override val failureExpectation: Option[FailureExpectation]) extends SingleCodeTestData { + override def checkCodeFragment: String = testCode + } + + object SimpleTestData { + def fromCode(code: String): SimpleTestData = { + val lines = code.strip.linesIterator.toSeq + + val testName = lines.head.trim.stripPrefix("//").trim + assert(testName.nonEmpty) + + val errors = + lines.zipWithIndex.collect { + case (line, lineNum) if line.contains("// Error") => + TestDataError(Some(lineNum + 1), None) + } + + SimpleTestData(testName, code.trim, FailureExpectation.fromErrors(errors, linesCovered = true)) + } + } + + abstract class withHighlightingTest(minScalaVersion: ScalaVersion) extends GeneratedTestSuiteFactory { + override type TD = SingleCodeTestData + final def makeActualTest(testData: SingleCodeTestData): Test = new SimpleHighlightingActualTest(testData, minScalaVersion) + } +} \ No newline at end of file diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/HashBuilderTest.scala b/src/test/scala/org/jetbrains/plugins/scala/util/HashBuilderTest.scala new file mode 100644 index 00000000..5706058a --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/HashBuilderTest.scala @@ -0,0 +1,29 @@ +package org.jetbrains.plugins.scala.util + +import junit.framework.TestCase +import org.jetbrains.plugins.scala.util.HashBuilder._ + +import java.util.Objects + +class HashBuilderTest extends TestCase { + + def testHashBuilder(): Unit = { + val a = "a" + val b = "b" + val c = "c" + + assertEqual(a: HashBuilder, Objects.hash(a)) + assertEqual(a #+ b, Objects.hash(a, b)) + assertEqual(a #+ a, Objects.hash(a, a)) + assertEqual(a #+ b #+ c, Objects.hash(a, b, c)) + assertEqual(c #+ b #+ a, Objects.hash(c, b, a)) + assertEqual(a #+ null #+ b, Objects.hash(a, null, b)) + assertEqual(1 #+ 2 #+ 3, Objects.hash(1: Integer, 2: Integer, 3: Integer)) + + assertEqual(1 #+ 2L #+ true, + Objects.hash(1: Integer, java.lang.Long.valueOf(2L), java.lang.Boolean.TRUE)) + } + + private def assertEqual(builder: HashBuilder, value: Int): Unit = + assert((builder: Int) == value) +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/IconUtils.scala b/src/test/scala/org/jetbrains/plugins/scala/util/IconUtils.scala new file mode 100644 index 00000000..a9571aea --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/IconUtils.scala @@ -0,0 +1,42 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.icons.AllIcons +import com.intellij.ui.{IconManager, LayeredIcon} +import com.intellij.ui.icons.CoreIconManager +import org.junit.Assert.fail + +import javax.swing.Icon + +object IconUtils { + + /** + * By default IconManager is deactivated and `com.intellij.ui.DummyIconManager` is used + * We need a proper IconManager implementation, in order layered icons are properly built in structure view tests. + * (see [[org.jetbrains.plugins.scala.util.BaseIconProvider.getIcon]]) + */ + def registerIconLayersInIconManager(): Unit = { + IconManager.getInstance() match { + case iconManager: CoreIconManager => + // workaround for IDEA-274148 (can remove it when the issue is fixed) + // copied from com.intellij.psi.impl.ElementPresentationUtil static initializer + val FLAGS_STATIC = 0x200 + val FLAGS_FINAL = 0x400 + val FLAGS_JUNIT_TEST = 0x2000 + val FLAGS_RUNNABLE = 0x4000 + + iconManager.registerIconLayer(FLAGS_STATIC, AllIcons.Nodes.StaticMark) + iconManager.registerIconLayer(FLAGS_FINAL, AllIcons.Nodes.FinalMark) + iconManager.registerIconLayer(FLAGS_JUNIT_TEST, AllIcons.Nodes.JunitTestMark) + iconManager.registerIconLayer(FLAGS_RUNNABLE, AllIcons.Nodes.RunnableMark) + case m => + fail(s"Unexpected icon manager: ${m.getClass} (expected ${classOf[CoreIconManager]})") + } + } + + + def createLayeredIcon(icons: Icon*): Icon = { + val result = new LayeredIcon(icons.length) + icons.zipWithIndex.foreach { case (icon, index) => result.setIcon(icon, index) } + result + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/JavaEnum.java b/src/test/scala/org/jetbrains/plugins/scala/util/JavaEnum.java new file mode 100644 index 00000000..6249f6d4 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/JavaEnum.java @@ -0,0 +1,5 @@ +package org.jetbrains.plugins.scala.util; + +public enum JavaEnum { + A, B, C, D +} diff --git a/src/test/scala/intellij/testfixtures/Markers.scala b/src/test/scala/org/jetbrains/plugins/scala/util/Markers.scala similarity index 91% rename from src/test/scala/intellij/testfixtures/Markers.scala rename to src/test/scala/org/jetbrains/plugins/scala/util/Markers.scala index e58239db..38628701 100644 --- a/src/test/scala/intellij/testfixtures/Markers.scala +++ b/src/test/scala/org/jetbrains/plugins/scala/util/Markers.scala @@ -2,7 +2,9 @@ package org.jetbrains.plugins.scala.util import com.intellij.openapi.util.TextRange import com.intellij.testFramework.EditorTestUtil +import junit.framework.TestCase import org.jetbrains.plugins.scala.extensions._ +import org.jetbrains.plugins.scala.util.assertions.AssertionMatchers import org.junit.Assert._ import scala.collection.immutable.SortedMap @@ -54,11 +56,11 @@ trait Markers { * line /start/ /start/ 1 /end/ content /start/ 2 /end/ /end/ */ def extractMarker( - inputText: String, - startMarker: String = this.startMarker, - endMarker: String = this.endMarker, - caretMarker: Option[String] = None - ): (String, Seq[TextRange]) = { + inputText: String, + startMarker: String = this.startMarker, + endMarker: String = this.endMarker, + caretMarker: Option[String] = None + ): (String, Seq[TextRange]) = { val (resultText, ranges) = extractMarkers(inputText, Seq(startMarker -> endMarker), caretMarker) (resultText, ranges.map(_._1)) } @@ -83,10 +85,10 @@ trait Markers { * @example see [[org.jetbrains.plugins.scala.util.MarkerUtilsTest.test_super_multi_nested]] for example */ def extractMarkers( - inputText: String, - markers: Seq[(String, String)], - caretMarker: Option[String] = None - ): (String, Seq[(TextRange, Int)]) = { + inputText: String, + markers: Seq[(String, String)], + caretMarker: Option[String] = None + ): (String, Seq[(TextRange, Int)]) = { val normalizedInput = inputText.withNormalizedSeparator val (ranges, idxAdjust) = markers.zipWithIndex @@ -137,7 +139,12 @@ trait Markers { val startIndexes = startReg.findAllMatchIn(inputText).map(_.start).toList val endIndexes = endReg.findAllMatchIn(inputText).map(_.start).toList assertEquals( - s"start & end markers counts are not equal\nstart: $startIndexes,\nend: $endIndexes\ntext: $inputText", + s"""start & end markers counts are not equal + |start indexes: $startIndexes, + |end indexes: $endIndexes + |start marker: $startMarker + |end marker: $endMarker + |text: $inputText""".stripMargin, startIndexes.size, endIndexes.size ) diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/ModificationTrackerTester.scala b/src/test/scala/org/jetbrains/plugins/scala/util/ModificationTrackerTester.scala new file mode 100644 index 00000000..92179371 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/ModificationTrackerTester.scala @@ -0,0 +1,27 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.openapi.project.Project +import org.jetbrains.plugins.scala.caches.ModTracker +import org.junit.Assert.assertEquals + +final class ModificationTrackerTester(project: Project) { + val modCountAnyBefore: Long = modCountAny + val modCountPhysicalBefore: Long = modCountPhysical + + def modCountAny: Long = ModTracker.anyScalaPsiChange.getModificationCount + + def modCountPhysical: Long = ModTracker.physicalPsiChange(project).getModificationCount + + def assertPsiModificationCountNotChanged(actionName: String): Unit = { + assertAnyPsiModificationCountNotChanged(actionName) + assertPhysicalPsiModificationCountNotChanged(actionName) + } + + private def assertAnyPsiModificationCountNotChanged(actionName: String): Unit = { + assertEquals(s"ModTracker.anyScalaPsiChange modification count has changed after $actionName", modCountAnyBefore, modCountAny) + } + + private def assertPhysicalPsiModificationCountNotChanged(actionName: String): Unit = { + assertEquals(s"ModTracker.physicalPsiChange modification count has changed after $actionName", modCountPhysicalBefore, modCountPhysical) + } +} \ No newline at end of file diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/NotNothing.scala b/src/test/scala/org/jetbrains/plugins/scala/util/NotNothing.scala new file mode 100644 index 00000000..3b668c55 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/NotNothing.scala @@ -0,0 +1,29 @@ +package org.jetbrains.plugins.scala.util + +import scala.annotation.implicitAmbiguous + +/** + * Scala hack to enforce in compile time check that a type parameter + * with this bound is __not__ `Nothing`. + * Consequently it may forbid omitting this parameter in method call, e.g. + * {{{ + * def get[T: NotNothing]: T = ??? + * + * def example { + * get[Int] // ok + * get[Nothing] // wrong + * get // wrong due to expansion in get[Nothing] + * } + * }}} + * + * @tparam T type param which can not be `Nothing` + */ +sealed trait NotNothing[T] + +private object NotNothing { + implicit def good[T]: NotNothing[T] = null + + @implicitAmbiguous("Specify generic type other than Nothing") + implicit def wrong1: NotNothing[Nothing] = null + implicit def wrong2: NotNothing[Nothing] = null +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/PsiFileTestUtil.scala b/src/test/scala/org/jetbrains/plugins/scala/util/PsiFileTestUtil.scala new file mode 100644 index 00000000..06918e17 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/PsiFileTestUtil.scala @@ -0,0 +1,45 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.{VfsUtil, VirtualFile} +import com.intellij.psi.{PsiDocumentManager, PsiFile, PsiManager} +import com.intellij.testFramework.LightPlatformTestCase +import org.jetbrains.plugins.scala.extensions.inWriteAction +import org.junit.Assert.{assertNotNull, assertTrue} + +import java.nio.charset.StandardCharsets +import java.nio.file.{Path, Paths} + +object PsiFileTestUtil { + def addFileToProject(fileName: String, text: String, project: Project): PsiFile = + addFileToProject(Paths.get(fileName), text, project) + + def addFileToProject(path: Path, text: String, project: Project): PsiFile = { + + val fileName = path.getFileName.toString + + def dirNames(path: Path): Seq[String] = + Iterator.tabulate(path.getNameCount - 1)(path.getName(_).toString).toSeq // last name in path is file itself + + def createDir(parent: VirtualFile, name: String): VirtualFile = parent.createChildDirectory(null, name) + + def createVFile(path: Path): VirtualFile = { + val sourceRoot = LightPlatformTestCase.getSourceRoot + val dir = dirNames(path).foldLeft(sourceRoot)(createDir) + val vFile = dir.createChildData(null, fileName) + vFile + } + + inWriteAction { + val vFile: VirtualFile = createVFile(path) + + VfsUtil.saveText(vFile, text) + val psiFile = PsiManager.getInstance(project).findFile(vFile) + assertNotNull("Can't create PsiFile for '" + fileName + "'. Unknown file type most probably.", vFile) + assertTrue(psiFile.isPhysical) + vFile.setCharset(StandardCharsets.UTF_8) + PsiDocumentManager.getInstance(project).commitAllDocuments() + psiFile + } + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/PsiSelectionUtil.scala b/src/test/scala/org/jetbrains/plugins/scala/util/PsiSelectionUtil.scala new file mode 100644 index 00000000..4e30a522 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/PsiSelectionUtil.scala @@ -0,0 +1,56 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.psi.PsiElement +import org.jetbrains.plugins.scala.extensions._ +import org.jetbrains.plugins.scala.lang.psi.api.base.ScReference +import org.jetbrains.plugins.scala.lang.psi.api.toplevel.ScNamedElement + +import scala.reflect.ClassTag + +trait PsiSelectionUtil { + type NamedElementPath = List[String] + + def selectElement[R <: PsiElement : ClassTag](elem: PsiElement, path: NamedElementPath, searchElement: Boolean = false): R = { + val typeName = implicitly[ClassTag[R]].runtimeClass.getName + def getInner(elem: PsiElement, path: List[String]): Either[String, R] = { + def pathString = "/" + path.mkString("/") + path match { + case name :: rest => + for { + candidate <- elem.depthFirst().collect { + case e: ScNamedElement if e.name == name => e + case r: ScReference if r.refName == name => r + } + found <- getInner(candidate, rest) + } { + return Right(found) + } + Left(s"Couldn't find path ${path.mkString("/")}") + case _ if searchElement => + val foundElements = elem.depthFirst().collect { case e: R => e }.to(LazyList) + foundElements match { + case LazyList(foundElement) => Right(foundElement) + case LazyList() => Left(s"Found no element of type $typeName in $pathString") + case elements => Left(s"Found ${elements.length} elements of type $typeName in $pathString:\n${elements.map(_.getText).mkString("\n")}") + } + case _ => + elem match { + case e: R => Right(e) + case e => Left(s"Found element at path $pathString, but it is of type ${e.getClass.getName}, not expected $typeName") + } + } + } + + getInner(elem, path) match { + case Right(e) => e + case Left(str) => throw new NoSuchElementException(str) + } + } + + def searchElement[R <: PsiElement : ClassTag](elem: PsiElement, path: NamedElementPath = List.empty): R = + selectElement[R](elem, path, searchElement = true) + + def path(path: String*): List[String] = path.toList +} + +object PsiSelectionUtil extends PsiSelectionUtil \ No newline at end of file diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/RevertableChange.scala b/src/test/scala/org/jetbrains/plugins/scala/util/RevertableChange.scala new file mode 100644 index 00000000..9645fa19 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/RevertableChange.scala @@ -0,0 +1,213 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.codeInsight.CodeInsightSettings +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ex.{ApplicationEx, ApplicationManagerEx} +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.registry.{Registry, RegistryValue} +import com.intellij.testFramework.UsefulTestCase +import org.jetbrains.plugins.scala.project.ModuleExt +import org.jetbrains.plugins.scala.project.settings.ScalaCompilerSettings +import org.jetbrains.plugins.scala.settings.ScalaProjectSettings +import org.jetbrains.plugins.scala.util.RevertableChange.CompositeRevertableChange + +trait RevertableChange { + + def applyChange(): Unit + + def revertChange(): Unit + + /** + * Applies the change and automatically reverts it when test disposable is disposed
+ * (it's done in tearDown method of test case) + * + * @param parentDisposable most commonly UsefulTestCase.getTestRootDisposable + */ + final def applyChange(parentDisposable: Disposable): Unit = { + applyChange() + Disposer.register(parentDisposable, () => { + revertChange() + }) + } + + final def applyChange(testCase: UsefulTestCase): Unit = { + applyChange(testCase.getTestRootDisposable) + } + + final def apply(body: => Any): Unit = + run(body) + + final def run(body: => Any): Unit = { + this.applyChange() + try + body + finally + this.revertChange() + } + + final def |+|(change: RevertableChange): RevertableChange = { + val changes = this match { + case composite: CompositeRevertableChange => composite.changes :+ change + case _ => Seq(this, change) + } + new CompositeRevertableChange(changes) + } +} + +object RevertableChange { + def combine(changes: Seq[RevertableChange]): RevertableChange = { + changes.foldLeft[RevertableChange](NoOpRevertableChange)(_ |+| _) + } + + object NoOpRevertableChange extends RevertableChange { + override def applyChange(): Unit = () + + override def revertChange(): Unit = () + } + + final class CompositeRevertableChange(val changes: Seq[RevertableChange]) extends RevertableChange { + override def applyChange(): Unit = changes.foreach(_.applyChange()) + + override def revertChange(): Unit = changes.reverse.foreach(_.revertChange()) + } + + def withModifiedRegistryValue(key: String, newValue: Boolean): RevertableChange = + withModifiedRegistryValueInternal[Boolean](key, newValue, _.asBoolean, _ setValue _) + + def withModifiedSystemProperty(key: String, newValue: String): RevertableChange = + withModifiedSetting[String]( + getter = System.getProperty(key), + setter = value => { + if (value == null) + System.clearProperty(key) + else + System.setProperty(key, value) + }, + newValue + ) + + def withModifiedRegistryValue(key: String, newValue: Int): RevertableChange = + withModifiedRegistryValueInternal[Int](key, newValue, _.asInteger(), _ setValue _) + + private def withModifiedRegistryValueInternal[A]( + key: String, + newValue: A, + getter: RegistryValue => A, + setter: (RegistryValue, A) => Unit + ): RevertableChange = + new RevertableChange { + private var before: Option[A] = None + + override def applyChange(): Unit = { + val registryValue = Registry.get(key) + before = Some(getter(registryValue)) + setter(registryValue, newValue) + } + + override def revertChange(): Unit = + before.foreach { oldValue => + setter(Registry.get(key), oldValue) + } + } + + def withModifiedSetting[Settings, T](instance: => Settings) + (value: T) + (get: Settings => T, set: (Settings, T) => Unit): RevertableChange = + new RevertableChange { + private var before: Option[T] = None + + override def applyChange(): Unit = { + before = Some(get(instance)) + set(instance, value) + } + + override def revertChange(): Unit = + before.foreach(set(instance, _)) + } + + def withModifiedSetting[A](getter: => A, setter: A => Unit, newValue: A): RevertableChange = + new RevertableChange { + private var before: Option[A] = None + + override def applyChange(): Unit = { + before = Some(getter) + setter(newValue) + } + + override def revertChange(): Unit = + before.foreach(setter) + } + + def withApplicationSettingsSaving: RevertableChange = new RevertableChange { + private var saveAllowedBefore: Boolean = _ + private lazy val application: ApplicationEx = ApplicationManagerEx.getApplicationEx + + override def applyChange(): Unit = { + saveAllowedBefore = application.isSaveAllowed + application.setSaveAllowed(true) + application.saveSettings() + } + + override def revertChange(): Unit = { + application.setSaveAllowed(saveAllowedBefore) + application.saveSettings() + } + } + + + def withModifiedCodeInsightSettings[T]( + get: CodeInsightSettings => T, + set: (CodeInsightSettings, T) => Unit, + value: T + ): RevertableChange = new RevertableChange { + private def instance: CodeInsightSettings = CodeInsightSettings.getInstance + + private var before: Option[T] = None + + override def applyChange(): Unit = { + before = Some(get(instance)) + set(instance, value) + } + + override def revertChange(): Unit = + before.foreach(set(instance, _)) + } + + def withModifiedScalaProjectSettings[T]( + project: Project, + get: ScalaProjectSettings => T, + set: (ScalaProjectSettings, T) => Unit, + value: T + ): RevertableChange = new RevertableChange { + private def instance: ScalaProjectSettings = ScalaProjectSettings.getInstance(project) + + private var before: Option[T] = None + + override def applyChange(): Unit = { + before = Some(get(instance)) + set(instance, value) + } + + override def revertChange(): Unit = + before.foreach(set(instance, _)) + } + + def withCompilerSettingsModified( + module: Module, + getModifiedCopy: ScalaCompilerSettings => ScalaCompilerSettings + ): RevertableChange = new RevertableChange { + private lazy val profile = module.scalaCompilerSettingsProfile + private lazy val oldSettings = profile.getSettings + + override def applyChange(): Unit = { + val newSettings = getModifiedCopy(oldSettings) + profile.setSettings(newSettings) + } + + override def revertChange(): Unit = { + profile.setSettings(oldSettings) + } + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/ShortCaretMarker.scala b/src/test/scala/org/jetbrains/plugins/scala/util/ShortCaretMarker.scala new file mode 100644 index 00000000..59eca98d --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/ShortCaretMarker.scala @@ -0,0 +1,9 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.testFramework.EditorTestUtil + +trait ShortCaretMarker { + val | : String = EditorTestUtil.CARET_TAG +} + +object ShortCaretMarker extends ShortCaretMarker diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/SoftAssert.scala b/src/test/scala/org/jetbrains/plugins/scala/util/SoftAssert.scala new file mode 100644 index 00000000..46bc9821 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/SoftAssert.scala @@ -0,0 +1,32 @@ +package org.jetbrains.plugins.scala.util + +import org.apache.commons.lang3.exception.ExceptionUtils +import org.hamcrest.{Matcher, MatcherAssert} + +import scala.util.control.NoStackTrace + +class SoftAssert { + private var errors = Seq.empty[AssertionError] + + final def assertAll(): Unit = + if (errors.nonEmpty) { + val stackTraces = errors.map(ExceptionUtils.getStackTrace) + val errorMessage = stackTraces.mkString("\n") + throw new AssertionError(errorMessage) with NoStackTrace + } + + final protected def assertThat[A](reason: String, + actual: A, + expected: Matcher[_ >: A]): Unit = + catchError(MatcherAssert.assertThat(reason, actual, expected)) + + // other assert methods... + + private def catchError(action: => Unit): Unit = + try { + action + } catch { + case assertionError: AssertionError => + errors :+= assertionError + } +} diff --git a/src/test/scala/intellij/testfixtures/TestUtils.scala b/src/test/scala/org/jetbrains/plugins/scala/util/TestUtils.scala similarity index 100% rename from src/test/scala/intellij/testfixtures/TestUtils.scala rename to src/test/scala/org/jetbrains/plugins/scala/util/TestUtils.scala diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/TextRangeUtils.scala b/src/test/scala/org/jetbrains/plugins/scala/util/TextRangeUtils.scala new file mode 100644 index 00000000..a1bd3a3f --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/TextRangeUtils.scala @@ -0,0 +1,13 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.openapi.util.TextRange + +object TextRangeUtils { + + object ImplicitConversions { + + import scala.language.implicitConversions + + implicit def tupleToTextRange(pair: (Int, Int)): TextRange = new TextRange(pair._1, pair._2) + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/TypeAnnotationSettings.scala b/src/test/scala/org/jetbrains/plugins/scala/util/TypeAnnotationSettings.scala new file mode 100644 index 00000000..6671f75e --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/TypeAnnotationSettings.scala @@ -0,0 +1,83 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.openapi.project.Project +import org.jetbrains.plugins.scala.lang.formatting.settings.ScalaCodeStyleSettings + +import java.util + +object TypeAnnotationSettings { + // TODO remove? + def set(project: Project, newSettings: ScalaCodeStyleSettings): Unit ={ + val settings: ScalaCodeStyleSettings = ScalaCodeStyleSettings.getInstance(project) + + import settings._ + + TYPE_ANNOTATION_PUBLIC_MEMBER = newSettings.TYPE_ANNOTATION_PUBLIC_MEMBER + TYPE_ANNOTATION_PROTECTED_MEMBER = newSettings.TYPE_ANNOTATION_PROTECTED_MEMBER + TYPE_ANNOTATION_PRIVATE_MEMBER = newSettings.TYPE_ANNOTATION_PRIVATE_MEMBER + TYPE_ANNOTATION_LOCAL_DEFINITION = newSettings.TYPE_ANNOTATION_LOCAL_DEFINITION + TYPE_ANNOTATION_FUNCTION_PARAMETER = newSettings.TYPE_ANNOTATION_FUNCTION_PARAMETER + TYPE_ANNOTATION_UNDERSCORE_PARAMETER = newSettings.TYPE_ANNOTATION_UNDERSCORE_PARAMETER + + TYPE_ANNOTATION_IMPLICIT_MODIFIER = newSettings.TYPE_ANNOTATION_IMPLICIT_MODIFIER + TYPE_ANNOTATION_UNIT_TYPE = newSettings.TYPE_ANNOTATION_UNIT_TYPE + TYPE_ANNOTATION_STRUCTURAL_TYPE = newSettings.TYPE_ANNOTATION_STRUCTURAL_TYPE + + TYPE_ANNOTATION_EXCLUDE_MEMBER_OF_ANONYMOUS_CLASS = newSettings.TYPE_ANNOTATION_EXCLUDE_MEMBER_OF_ANONYMOUS_CLASS + TYPE_ANNOTATION_EXCLUDE_MEMBER_OF_PRIVATE_CLASS = newSettings.TYPE_ANNOTATION_EXCLUDE_MEMBER_OF_PRIVATE_CLASS + TYPE_ANNOTATION_EXCLUDE_CONSTANT = newSettings.TYPE_ANNOTATION_EXCLUDE_CONSTANT + TYPE_ANNOTATION_EXCLUDE_WHEN_TYPE_IS_STABLE = newSettings.TYPE_ANNOTATION_EXCLUDE_WHEN_TYPE_IS_STABLE + TYPE_ANNOTATION_EXCLUDE_IN_TEST_SOURCES = newSettings.TYPE_ANNOTATION_EXCLUDE_IN_TEST_SOURCES + TYPE_ANNOTATION_EXCLUDE_IN_DIALECT_SOURCES = newSettings.TYPE_ANNOTATION_EXCLUDE_IN_DIALECT_SOURCES + + TYPE_ANNOTATION_EXCLUDE_MEMBER_OF = new util.HashSet(newSettings.TYPE_ANNOTATION_EXCLUDE_MEMBER_OF) + TYPE_ANNOTATION_EXCLUDE_ANNOTATED_WITH = new util.HashSet(newSettings.TYPE_ANNOTATION_EXCLUDE_ANNOTATED_WITH) + TYPE_ANNOTATION_EXCLUDE_WHEN_TYPE_MATCHES = new util.HashSet(newSettings.TYPE_ANNOTATION_EXCLUDE_WHEN_TYPE_MATCHES) + } + + def alwaysAddType(settings: ScalaCodeStyleSettings): ScalaCodeStyleSettings ={ + val coppedSettings = settings.clone().asInstanceOf[ScalaCodeStyleSettings] + + import coppedSettings._ + + TYPE_ANNOTATION_PUBLIC_MEMBER = true + TYPE_ANNOTATION_PROTECTED_MEMBER = true + TYPE_ANNOTATION_PRIVATE_MEMBER = true + TYPE_ANNOTATION_LOCAL_DEFINITION = true + TYPE_ANNOTATION_FUNCTION_PARAMETER = true + TYPE_ANNOTATION_UNDERSCORE_PARAMETER = true + + // The above values should be enough + TYPE_ANNOTATION_IMPLICIT_MODIFIER = false + TYPE_ANNOTATION_UNIT_TYPE = false + TYPE_ANNOTATION_STRUCTURAL_TYPE = false + + TYPE_ANNOTATION_EXCLUDE_MEMBER_OF_ANONYMOUS_CLASS = false + TYPE_ANNOTATION_EXCLUDE_MEMBER_OF_PRIVATE_CLASS = false + TYPE_ANNOTATION_EXCLUDE_CONSTANT = false + TYPE_ANNOTATION_EXCLUDE_WHEN_TYPE_IS_STABLE = false + TYPE_ANNOTATION_EXCLUDE_IN_TEST_SOURCES = false + TYPE_ANNOTATION_EXCLUDE_IN_DIALECT_SOURCES = false + + TYPE_ANNOTATION_EXCLUDE_MEMBER_OF = new util.HashSet() + TYPE_ANNOTATION_EXCLUDE_ANNOTATED_WITH = new util.HashSet() + TYPE_ANNOTATION_EXCLUDE_WHEN_TYPE_MATCHES = new util.HashSet() + + coppedSettings + } + + def noTypeAnnotationForPublic(settings: ScalaCodeStyleSettings): ScalaCodeStyleSettings ={ + settings.TYPE_ANNOTATION_PUBLIC_MEMBER = false + settings + } + + def noTypeAnnotationForProtected(settings: ScalaCodeStyleSettings): ScalaCodeStyleSettings ={ + settings.TYPE_ANNOTATION_PROTECTED_MEMBER = false + settings + } + + def noTypeAnnotationForLocal(settings: ScalaCodeStyleSettings): ScalaCodeStyleSettings ={ + settings.TYPE_ANNOTATION_LOCAL_DEFINITION = false + settings + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/WriteCommandActionEx.scala b/src/test/scala/org/jetbrains/plugins/scala/util/WriteCommandActionEx.scala new file mode 100644 index 00000000..20c0b509 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/WriteCommandActionEx.scala @@ -0,0 +1,11 @@ +package org.jetbrains.plugins.scala.util + +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.project.Project + +object WriteCommandActionEx { + //the method is required to workaround "can't overload method" error when using WriteCommandAction from scala + def runWriteCommandAction(project: Project, runnable: Runnable): Unit = { + WriteCommandAction.runWriteCommandAction(project, runnable) + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/assertions/AssertionMatchers.scala b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/AssertionMatchers.scala new file mode 100644 index 00000000..4f379083 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/AssertionMatchers.scala @@ -0,0 +1,23 @@ +package org.jetbrains.plugins.scala.util.assertions + +import org.jetbrains.annotations.Nullable +import org.junit.Assert._ + +trait AssertionMatchers { + + implicit class AssertMatchersExt[T](@Nullable private val actual: T) { + def shouldBe(@Nullable expected: T): Unit = (actual, expected) match { + case (actual: Double, expected: Double) => assertEquals(expected, actual, 0.01) + case (actual: Float, expected: Float) => assertEquals(expected, actual, 0.01) + case (actual, expected) => assertEquals(expected, actual) + } + + def shouldNotBe(@Nullable notExpected: T): Unit = (actual, notExpected) match { + case (actual: Double, notExpected: Double) => assertNotEquals(notExpected, actual, 0.01) + case (actual: Float, notExpected: Float) => assertNotEquals(notExpected, actual, 0.01) + case (actual, notExpected) => assertNotEquals(notExpected, actual) + } + } +} + +object AssertionMatchers extends AssertionMatchers diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/assertions/CollectionsAssertions.scala b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/CollectionsAssertions.scala new file mode 100644 index 00000000..2233e9fe --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/CollectionsAssertions.scala @@ -0,0 +1,22 @@ +package org.jetbrains.plugins.scala.util.assertions + +import org.junit.ComparisonFailure + +trait CollectionsAssertions { + + def assertCollectionEquals[T, C[_] <: Iterable[_]](expected: C[T], actual: C[T]): Unit = + assertCollectionEquals("", expected, actual) + + def assertCollectionEquals[T, C[_] <: Iterable[_]](message: String, expected: C[T], actual: C[T]): Unit = + if (expected != actual) + throw new ComparisonFailure( + message, + //NOTE: technically it's not very good to use `orNull` + //it's impossible to distinguish the case when we have collection with single element `null` and actual `null` + //But it only affects the diff view nad this should be fine in 99.99% cases + Option(expected).map(_.mkString("\n")).orNull, + Option(actual).map(_.mkString("\n")).orNull, + ) +} + +object CollectionsAssertions extends CollectionsAssertions diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/assertions/ExceptionAssertions.scala b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/ExceptionAssertions.scala new file mode 100644 index 00000000..fe7947d0 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/ExceptionAssertions.scala @@ -0,0 +1,46 @@ +package org.jetbrains.plugins.scala.util.assertions + +import org.junit.ComparisonFailure + +import scala.reflect.ClassTag + +trait ExceptionAssertions { + + import org.junit.Assert._ + + def assertExceptionMessage[E <: Throwable: ClassTag](expectedMessage: Option[String])(code: => Unit): Unit = { + val expectedExceptionName = implicitly[ClassTag[E]].runtimeClass.getName + try { + code + } catch { + case e: E => + expectedMessage.foreach { expectedMsg => + if (expectedMsg != e.getMessage) { + System.err.println("Wrong message in exception:") + System.err.println(e.getMessage) + e.printStackTrace(System.err) + throw new ComparisonFailure("", expectedMsg, e.getMessage) + } + } + return + + case e: Throwable => + throw new AssertionError(s"Expected exception $expectedExceptionName but ${e.getClass.getName} was thrown", e) + } + + expectedMessage match { + case Some(msg) => fail(s"$expectedExceptionName with message '$msg' should have been thrown") + case None => fail(s"$expectedExceptionName should have been thrown") + } + + } + + def assertExceptionMessage[E <: Throwable: ClassTag](expectedMessage: String)(code: => Unit): Unit = + assertExceptionMessage(Some(expectedMessage))(code) + + def assertException[E <: Throwable: ClassTag](code: => Unit): Unit = + assertExceptionMessage(None)(code) +} + + +object ExceptionAssertions extends ExceptionAssertions \ No newline at end of file diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/assertions/MatcherAssertions.scala b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/MatcherAssertions.scala new file mode 100644 index 00000000..49df8e5a --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/MatcherAssertions.scala @@ -0,0 +1,61 @@ +package org.jetbrains.plugins.scala.util.assertions + +import org.jetbrains.annotations.Nls +import org.jetbrains.plugins.scala.annotator.Message +import org.jetbrains.plugins.scala.base.FailableTest +import org.junit.Assert + +import scala.reflect.ClassTag + +trait MatcherAssertions extends FailableTest { + + def assertNothing[T](actual: Option[T]): Unit = + assertMatches(actual) { + case Nil => + } + + def assertMatches[T](actual: Option[T])(pattern: PartialFunction[T, Unit]): Unit = + actual match { + case Some(value) => + def message = if (shouldPass) { + val actualValueFancy = value match { + case seq: Seq[_] => seq.mkString(s"${seq.getClass.getSimpleName}(\n ", ",\n ", "\n)") + case v => v.toString + } + "actual: " + actualValueFancy + } else { + failingPassed + } + Assert.assertTrue(message, shouldPass == pattern.isDefinedAt(value)) + case None => Assert.assertFalse(shouldPass) + } + + def assertNothing[T](actual: T): Unit = + assertNothing(Some(actual)) + + def assertMatches[T](actual: T)(pattern: PartialFunction[T, Unit]): Unit = + assertMatches(Some(actual))(pattern) + + def assertMessages(actual: List[Message])(expected: Message*): Unit = + assertEqualsFailable(expected.mkString("\n"), actual.mkString("\n")) + + def assertMessagesSorted(actual: List[Message])(expected: Message*): Unit = + assertMessages(actual.sorted)(expected.sorted: _*) + + def assertIsA[T](obj: Object)(implicit classTag: ClassTag[T]): T = + if (classTag.runtimeClass.isInstance(obj)) { + obj.asInstanceOf[T] + } else { + Assert.fail(s"wrong object class\nexpected ${classTag.runtimeClass.getName}\nactual:${obj.getClass.getName}").asInstanceOf[Nothing] + } + + case class ContainsPattern(fragment: String) { + def unapply(s: String): Boolean = s.contains(fragment) + } + + case class BundleMessagePattern(@Nls message: String) { + def unapply(text: String): Boolean = text == message + } +} + +object MatcherAssertions extends MatcherAssertions \ No newline at end of file diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/assertions/PsiAssertions.scala b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/PsiAssertions.scala new file mode 100644 index 00000000..92589182 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/PsiAssertions.scala @@ -0,0 +1,30 @@ +package org.jetbrains.plugins.scala.util.assertions + +import com.intellij.psi.{PsiDocumentManager, PsiErrorElement, PsiFile} +import org.jetbrains.plugins.scala.extensions.{IterableOnceExt, PsiElementExt} +import org.jetbrains.plugins.scala.util.assertions.CollectionsAssertions.assertCollectionEquals + +trait PsiAssertions { + def assertNoParserErrors(file: PsiFile): Unit = { + val errorElements = file.elements.filterByType[PsiErrorElement].toSeq + + if (errorElements.nonEmpty) { + val document = PsiDocumentManager.getInstance(file.getProject).getDocument(file) + val fileText = file.getText + val errorsReadable: Seq[String] = errorElements.map { e => + val range = e.getTextRange + val line = document.getLineNumber(range.getStartOffset) + val code = fileText.substring(range.getStartOffset, range.getEndOffset) + val errorMessage = e.getErrorDescription + s"$line: $code - $errorMessage" + } + assertCollectionEquals( + s"Expected no parser errors in file ${file.getName}", + Seq[String](), + errorsReadable + ) + } + } +} + +object PsiAssertions extends PsiAssertions \ No newline at end of file diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/assertions/StringAssertions.scala b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/StringAssertions.scala new file mode 100644 index 00000000..b74c7f9e --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/StringAssertions.scala @@ -0,0 +1,64 @@ +package org.jetbrains.plugins.scala.util.assertions + +import org.junit.Assert._ + +import scala.util.matching.Regex + +trait StringAssertions { + + def assertStringMatches(string: String, regex: Regex): Unit = + regex.findAllMatchIn(string).toSeq match { + case Seq(_) => + case _ => + fail( + s"""string doesn't match regular expression: + |regex: $regex + |actual: $string""".stripMargin + ) + } + + def assertStringNotMatches(string: String, regex: Regex): Unit = + regex.findAllMatchIn(string).toSeq match { + case Seq() => + case matches => + fail( + s"""string should't match regular expression: + |regex: $regex + |actual: $string + |matches: + |${matches.map(_.matched).mkString("\n")}""".stripMargin + ) + } + + def assertStartsWith(string: String, prefix: String): Unit = + if (!string.startsWith(prefix)) { + fail( + s"""input string doesn't start with expected prefix + |prefix: `${display(prefix)}` + |input: `${display(string)}` + |""".stripMargin + ) + } + + def assertEndsWith(string: String, suffix: String): Unit = + if (!string.endsWith(suffix)) { + fail( + s"""input string doesn't end with expected suffix + |suffix: `${display(suffix)}` + |input: `${display(string)}` + |""".stripMargin + ) + } + + def assertIsBlank(string: String): Unit = + if (!string.trim.isEmpty) + fail(s"expected blank string but got:\n${string}") + + private def display(str: String): String = + str.replace("\\n", "\\\\n") + .replace("\\r", "\\\\r") + .replace("\n", "\\n") + .replace("\r", "\\r") +} + +object StringAssertions extends StringAssertions diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/assertions/package.scala b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/package.scala new file mode 100644 index 00000000..a759e002 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/assertions/package.scala @@ -0,0 +1,19 @@ +package org.jetbrains.plugins.scala.util + +import org.junit.Assert.fail + +import scala.util.{Failure, Success, Try} + +package object assertions { + + def failWithCause(message: String, cause: Throwable): Nothing = + throw new AssertionError(message, cause) + + def assertFails(body: => Unit): Unit = + Try(body) match { + case Failure(_: AssertionError) => // as expected + case Failure(exception) => throw exception + case Success(_) => + fail("Test is expected to fail but is passed successfully") + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/dependencymanager/TestDependencyManager.scala b/src/test/scala/org/jetbrains/plugins/scala/util/dependencymanager/TestDependencyManager.scala new file mode 100644 index 00000000..c6110d54 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/dependencymanager/TestDependencyManager.scala @@ -0,0 +1,11 @@ +package org.jetbrains.plugins.scala.util.dependencymanager + +import org.jetbrains.plugins.scala.DependencyManagerBase + +object TestDependencyManager extends DependencyManagerBase { + + // from Michael M.: this blacklist is in order that tested libraries do not transitively fetch `scala-library`, + // which is loaded in a special way in tests via org.jetbrains.plugins.scala.base.libraryLoaders.ScalaSDKLoader + //TODO: should we add scala3-* here? + override val artifactBlackList: Set[String] = Set("scala-library", "scala-reflect", "scala-compiler") +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/dependencymanager/TestDependencyManagerForSbt.scala b/src/test/scala/org/jetbrains/plugins/scala/util/dependencymanager/TestDependencyManagerForSbt.scala new file mode 100644 index 00000000..46c3f0c4 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/dependencymanager/TestDependencyManagerForSbt.scala @@ -0,0 +1,30 @@ +package org.jetbrains.plugins.scala.util.dependencymanager + +import org.jetbrains.plugins.scala.DependencyManagerBase +import org.jetbrains.plugins.scala.DependencyManagerBase.{IvyResolver, Resolver} +import org.jetbrains.plugins.scala.project.Version + +/** + * Adds additional resolver for typesafe repository
+ * It's needed to be able to resolve sbt 0.13 releases, which is not published to maven central + */ +final class TestDependencyManagerForSbt(private val sbtVersion: Version) extends DependencyManagerBase { + + private val includeTypesafeRepo = sbtVersion < Version("1.0.0") + + override protected def resolvers: Seq[DependencyManagerBase.Resolver] = { + val extraResolvers = if (includeTypesafeRepo) Seq(Resolver.TypesafeReleases) else Nil + super.resolvers ++ extraResolvers + } + + override def equals(other: Any): Boolean = other match { + case that: TestDependencyManagerForSbt => + sbtVersion == that.sbtVersion + case _ => false + } + + override def hashCode(): Int = { + val state = Seq(sbtVersion) + state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + } +} \ No newline at end of file diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/extensions.scala b/src/test/scala/org/jetbrains/plugins/scala/util/extensions.scala new file mode 100644 index 00000000..91f0110e --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/extensions.scala @@ -0,0 +1,26 @@ +package org.jetbrains.plugins.scala.util + +import org.junit.ComparisonFailure + +object extensions { + + implicit class ComparisonFailureOps(private val failure: ComparisonFailure) extends AnyVal { + + // add "before" state to conveniently view failed tests + def withBeforePrefix(textBefore: String): ComparisonFailure = + new org.junit.ComparisonFailure( + failure.getMessage, + addBeforePrefix(textBefore, failure.getExpected), + addBeforePrefix(textBefore, failure.getActual) + ) + + // add "before" state to conveniently view failed tests + private def addBeforePrefix(textBefore: String, textAfter: String)= { + s"""<<>>: + |$textBefore + |---------------------------------------------------- + |<<>>: + |$textAfter""".stripMargin + } + } +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/matchers/HamcrestMatchers.scala b/src/test/scala/org/jetbrains/plugins/scala/util/matchers/HamcrestMatchers.scala new file mode 100644 index 00000000..d649db10 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/matchers/HamcrestMatchers.scala @@ -0,0 +1,65 @@ +package org.jetbrains.plugins.scala.util.matchers + +import org.hamcrest.Matcher + +import scala.math.Ordering.Implicits._ + +trait HamcrestMatchers { + + def hasSize(size: Int): Matcher[Iterable[_]] = new ScalaBaseMatcher[Iterable[_]] { + override protected def valueMatches(actualValue: Iterable[_]): Boolean = + actualValue.size == size + + override protected def description: String = + s"collection of size $size" + } + + def emptyCollection: Matcher[Iterable[_]] = + hasSize(0) + + /** + * Checks if actual value is greater than specified + */ + def greaterThan[V: Ordering](value: V): Matcher[V] = new ScalaBaseMatcher[V] { + override protected def valueMatches(actualValue: V): Boolean = + actualValue > value + + override protected def description: String = + s"greater than $value" + } + + /** + * Checks if actual value is less than specified + */ + def lessThan[V: Ordering](value: V): Matcher[V] = new ScalaBaseMatcher[V] { + + override protected def valueMatches(actualValue: V): Boolean = + actualValue < value + + override protected def description: String = + s"less than $value" + } + + /** + * Checks if every map value satisfies to corresponding matcher. + */ + def everyValue[K, V](matchers: Map[K, Matcher[V]]): Matcher[Map[K, V]] = new ScalaBaseMatcher[Map[K, V]] { + override protected def valueMatches(actualValue: Map[K, V]): Boolean = { + val keys = actualValue.keySet | matchers.keySet + keys.forall { key => + (actualValue.get(key), matchers.get(key)) match { + case (Some(actual), Some(matcher)) => matcher.matches(actual) + case _ => false + } + } + } + + override protected def description: String = + matchers.toString + } + + def everyValueGreaterThanIn[K, V: Ordering](value: Map[K, V]): Matcher[Map[K, V]] = + everyValue(value.view.mapValues(greaterThan(_)).toMap) +} + +object HamcrestMatchers extends HamcrestMatchers diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/matchers/ScalaBaseMatcher.scala b/src/test/scala/org/jetbrains/plugins/scala/util/matchers/ScalaBaseMatcher.scala new file mode 100644 index 00000000..dc679502 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/matchers/ScalaBaseMatcher.scala @@ -0,0 +1,29 @@ +package org.jetbrains.plugins.scala.util.matchers + +import org.hamcrest.{BaseMatcher, Description} + +/** + * Base matcher for using with [[org.junit.Assert.assertThat]] or [[org.junit.Assume.assumeThat]]. + * Please use extending of this class instead of [[org.hamcrest.BaseMatcher]]. + * + * @tparam V type of the actual and expected value + */ +abstract class ScalaBaseMatcher[V] + extends BaseMatcher[V] { + + /** + * Checks if actual value is matches matcher. + */ + protected def valueMatches(actualValue: V): Boolean + + /** + * Text description of the matcher. + */ + protected def description: String + + final override def matches(item: Any): Boolean = + valueMatches(item.asInstanceOf[V]) + + final override def describeTo(desc: Description): Unit = + desc.appendText(description) +} diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/MultipleScalaVersionsRunner.scala b/src/test/scala/org/jetbrains/plugins/scala/util/runners/MultipleScalaVersionsRunner.scala new file mode 100644 index 00000000..9de25331 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/MultipleScalaVersionsRunner.scala @@ -0,0 +1,331 @@ +package org.jetbrains.plugins.scala.util.runners + +import com.intellij.pom.java.{LanguageLevel => JdkVersion} +import junit.extensions.TestDecorator +import junit.framework.{Test, TestCase, TestResult, TestSuite} +import org.jetbrains.plugins.scala.ScalaVersion +import org.jetbrains.plugins.scala.base.{InjectableJdk, ScalaSdkOwner} +import org.jetbrains.plugins.scala.util.teamcity.TeamcityUtils +import org.jetbrains.plugins.scala.util.teamcity.TeamcityUtils.Status.Warning +import org.junit.experimental.categories.Category +import org.junit.internal.runners.JUnit38ClassRunner +import org.junit.runner.manipulation.{Filter, Filterable} +import org.junit.runner.{Describable, Description} + +import java.lang.annotation.Annotation +import java.util +import scala.annotation.{tailrec, unused} +import scala.jdk.CollectionConverters._ + +class MultipleScalaVersionsRunner(private val myTest: Test, klass: Class[_]) extends JUnit38ClassRunner(myTest) { + + def this(klass: Class[_]) = + this(MultipleScalaVersionsRunner.testSuite(klass.asSubclass(classOf[TestCase])), klass) + + override def getDescription: Description = { + val description = MultipleScalaVersionsRunner.makeDescription(klass, myTest) + //debugLog(description) + description + } + + override def filter(filter: Filter): Unit = + super.filter(filter) +} + +private object MultipleScalaVersionsRunner { + + private val DefaultScalaVersionsToRun: Seq[TestScalaVersion] = + Seq( + TestScalaVersion.Scala_2_11, + TestScalaVersion.Scala_2_12, + TestScalaVersion.Scala_2_13, + ) + + private val DefaultJdkVersionToRun: TestJdkVersion = + TestJdkVersion.from(InjectableJdk.DefaultJdk) + + lazy val filterJdkVersionRegistry: Option[TestJdkVersion] = { + val result = Option(System.getProperty("filter.test.jdk.version")).map(TestJdkVersion.valueOf) + result.foreach(v => TeamcityUtils.logUnderTeamcity(s"MultipleScalaVersionsRunner: running jdk filter: $v", status = Warning)) + result + } + + class MyBaseTestSuite(name: String) extends TestSuite(name) with Filterable { + + override def filter(filter: Filter): Unit = { + var mutedTestsIndexes = List.empty[Int] + + myTestsScala.zipWithIndex.foreach { + case (test: Filterable, _) => + test.filter(filter) + case (test, testIdx) => + val description = makeDescription(test.getClass, test) + val shouldRun = filter.shouldRun(description) + if (!shouldRun) { + mutedTestsIndexes ::= testIdx + } + } + + //the list is already in the reversed order + mutedTestsIndexes.foreach(myTests.remove) + } + + private val myTests: util.List[Test] = new util.ArrayList[Test] + private def myTestsScala: Seq[Test] = { + //noinspection ScalaRedundantCast + // asInstanceOf is needed. we have multiple junit versions in compiler classpath (3.8, 4.11, 4.12) and jar files order is undefined. See: SCL-18768 + myTests.asScala.toSeq.asInstanceOf[Seq[Test]] + } + + override def addTest(test: Test): Unit = { + myTests.add(test) + } + + override def addTestSuite(testClass: Class[_ <: TestCase]): Unit = { + super.addTestSuite(testClass) + } + + override def tests(): util.Enumeration[Test] = + util.Collections.enumeration(myTests) + + override def testAt(index: Int): Test = + myTests.get(index) + + override def testCount: Int = + myTests.size + + override def run(result: TestResult): Unit = { + var continue = true + for (each <- myTestsScala if continue) { + if (result.shouldStop) { + continue = false + } + else { + runTest(each, result) + } + } + } + } + + private case class ScalaVersionTestSuite(name: String) extends MyBaseTestSuite(name) { + def this() = this(null: String) + def this(version: ScalaVersion) = this(sanitize(s"(scala ${version.minor})")) + def this(version: ScalaVersion, jdkVersion: JdkVersion) = this(sanitize(s"(scala ${version.minor} $jdkVersion)")) + } + + private case class JdkVersionTestSuite(name: String) extends MyBaseTestSuite(name) { + def this() = this(null: String) + def this(version: JdkVersion) = this(sanitize(s"(jdk ${version.toString})")) + } + + // SCL-21849 + private case class IndexingModeTestSuite(name: String) extends MyBaseTestSuite(name) { + def this(indexingMode: TestIndexingMode) = this(s"(${indexingMode.label})") + } + + def testSuite(klass: Class[_ <: TestCase]): TestSuite = { + assert(classOf[ScalaSdkOwner].isAssignableFrom(klass)) + + val suite = new MyBaseTestSuite(klass.getName) + + val classScalaVersions = scalaVersionsToRun(klass) + val classJdkVersions = jdkVersionsToRun(klass) + assert(classScalaVersions.nonEmpty, "at least one scala version should be specified") + assert(classJdkVersions.nonEmpty, "at least one jdk version should be specified") + + val filterScalaVersionAnnotation = findAnnotation(klass, classOf[RunWithScalaVersionsFilter]).map(_.value.toSeq) + val filterJdkVersionAnnotation = findAnnotation(klass, classOf[RunWithJdkVersionsFilter]).map(_.value.toSeq) + + val runWithScalaVersion: Option[Seq[TestScalaVersion]] = + filterScalaVersionAnnotation + val runWithJdkVersion: Option[Seq[TestJdkVersion]] = { + (filterJdkVersionAnnotation, filterJdkVersionRegistry.map(Seq(_))) match { + case (Some(a), Some(b)) => Some(a.intersect(b)) + case (Some(a), None) => Some(a) + case (None, Some(b)) => Some(b) + case (None, None) => None + } + } + + def filterScalaVersion(version: TestScalaVersion): Boolean = + runWithScalaVersion.forall(_.contains(version)) + def filterJdkVersion(version: TestJdkVersion): Boolean = + runWithJdkVersion.forall(_.contains(version)) + + val allTestCases: Seq[(TestCase, ScalaVersion, JdkVersion, TestIndexingMode)] = { + val collected = new ScalaVersionAwareTestsCollector(klass, classScalaVersions, classJdkVersions).collectTests() + collected.collect { case (test, sv, jv, im) if filterScalaVersion(sv) && filterJdkVersion(jv) => + (test, sv.toProductionVersion, jv.toProductionVersion, im) + } + } + + val childTests = childTestsByScalaVersion(allTestCases) + // val childTests = childTestsByName(allTests) + childTests.foreach { childTest => + suite.addTest(childTest) + } + + suite + } + +// private def childTestsByName(testsCases: Seq[(TestCase, ScalaVersion, JdkVersion)]): Seq[Test] = { +// val nameToTests: Map[String, Seq[(TestCase, ScalaVersion)]] = testsCases.groupBy(_._1.getName) +// +// for { +// (testName, tests: Seq[(TestCase, ScalaVersion)]) <- nameToTests.toSeq.sortBy(_._1) +// } yield { +// if (tests.size == 1) tests.head._1 +// else { +// val suite = new framework.TestSuite() +// suite.setName(testName) +// tests.sortBy(_._2).foreach { case (t, version) => +// t.setName(testName + "." + sanitize(version.minor)) +// suite.addTest(t) +// } +// suite +// } +// } +// } + + private def childTestsByScalaVersion(testCases: Seq[(TestCase, ScalaVersion, JdkVersion, TestIndexingMode)]): Seq[Test] = { + val scalaVersionToTests: Map[ScalaVersion, Seq[Test]] = + testCases.groupBy(_._2) + .view + .mapValues(_.map(t => (t._1, t._3, t._4))) + .mapValues(childTestsByJdkVersion) + .toMap + + if (scalaVersionToTests.size == 1) { + scalaVersionToTests.head._2 + } else { + for { + (version, tests) <- scalaVersionToTests.toSeq.sortBy(_._1) + if tests.nonEmpty + } yield { + val firstTest = tests.head + val suite = firstTest match { + case _: JdkVersionTestSuite | _: IndexingModeTestSuite => + new ScalaVersionTestSuite(version) + case s: ScalaSdkOwner => + // if only one jdk version is used, display it in the test name + val jdkVersion = s.testProjectJdkVersion + new ScalaVersionTestSuite(version, jdkVersion) + case _: TestCase => + /** + * In case test initialization in runner has failed
+ * @see [[junit.framework.TestSuite.warning]] + * @see [[junit.framework.TestSuite.createTest]] + */ + new ScalaVersionTestSuite(version) + } + tests.foreach(suite.addTest) + suite + } + } + } + + private def childTestsByJdkVersion(testCases: Seq[(TestCase, JdkVersion, TestIndexingMode)]): Seq[Test] = { + val jdkVersionToTests: Map[JdkVersion, Seq[Test]] = + testCases.groupBy(_._2) + .view + .mapValues(_.map(t => (t._1, t._3))) + .mapValues(childTestsByIndexingMode) + .toMap + + if (jdkVersionToTests.size == 1) jdkVersionToTests.head._2 else { + for { + (version, tests) <- jdkVersionToTests.toSeq.sortBy(_._1) + if tests.nonEmpty + } yield { + val suite = new JdkVersionTestSuite(version) + tests.foreach(suite.addTest) + suite + } + } + } + + private def childTestsByIndexingMode(testCases: Seq[(TestCase, TestIndexingMode)]): Seq[Test] = { + val indexingModeToTests: Map[TestIndexingMode, Seq[Test]] = + testCases.groupBy(_._2) + .view + .mapValues(_.map(_._1)) + .toMap + + if (indexingModeToTests.size == 1) indexingModeToTests.head._2 else { + for { + (indexingMode, tests) <- indexingModeToTests.toSeq.sortBy(_._1) + if tests.nonEmpty + } yield { + val suite = new IndexingModeTestSuite(indexingMode) + tests.foreach(suite.addTest) + suite + } + } + } + + private def scalaVersionsToRun(klass: Class[_ <: TestCase]): Seq[TestScalaVersion] = { + val annotation = findAnnotation(klass, classOf[RunWithScalaVersions]) + annotation + .map(_.value.toSeq) + .getOrElse(DefaultScalaVersionsToRun) + } + + private def jdkVersionsToRun(klass: Class[_ <: TestCase]): Seq[TestJdkVersion] = { + val annotation = findAnnotation(klass, classOf[RunWithJdkVersions]) + annotation + .map(_.value.toSeq) + .getOrElse(Seq(DefaultJdkVersionToRun)) + } + + private[runners] def findAnnotation[T <: Annotation](klass: Class[_], annotationClass: Class[T]): Option[T] = { + @tailrec + def inner(c: Class[_]): Annotation = c.getAnnotation(annotationClass) match { + case null => + c.getSuperclass match { + case null => null + case parent => inner(parent) + } + case annotation => annotation + } + + Option(inner(klass).asInstanceOf[T]) + } + + @unused + private def debugLog(d: Description, deep: Int = 0): Unit = { + val annotations = d.getAnnotations.asScala.map(_.annotationType.getName).mkString(",") + val details = s"${d.getMethodName}, ${d.getClassName}, ${d.getTestClass}, $annotations" + val prefix = "##" + " " * deep + System.out.println(s"$prefix ${d.toString} ($details)") + d.getChildren.forEach(debugLog(_, deep + 1)) + } + + // Copied from JUnit38ClassRunner, added "Category" annotation propagation for ScalaVersionTestSuite + private def makeDescription(klass: Class[_], test: Test): Description = test match { + case ts: TestSuite => + val name = Option(ts.getName).getOrElse(createSuiteDescriptionName(ts)) + val annotations = findAnnotation(klass, classOf[Category]).toSeq + val description = Description.createSuiteDescription(name, annotations: _*) + ts.tests.asScala.foreach { childTest => + // compiler fails on TeamCity without this case, no idea why + //noinspection ScalaRedundantCast + val childDescription = makeDescription(klass, childTest.asInstanceOf[Test]) + description.addChild(childDescription) + } + description + case tc: TestCase => Description.createTestDescription(tc.getClass, tc.getName) + case adapter: Describable => adapter.getDescription + case decorator: TestDecorator => makeDescription(klass, decorator.getTest) + case _ => Description.createSuiteDescription(test.getClass) + } + + private def createSuiteDescriptionName(ts: TestSuite): String = { + val count = ts.countTestCases + val example = if (count == 0) "" else " [example: %s]".format(ts.testAt(0)) + "TestSuite with %s tests%s".format(count, example) + } + + // dot is treated as a package separator by IntelliJ which causes broken rendering in tests tree + private def sanitize(testName: String): String = testName.replace(".", "_") +} + diff --git a/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithAllIndexingModes.java b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithAllIndexingModes.java new file mode 100644 index 00000000..7e4d3622 --- /dev/null +++ b/src/test/scala/org/jetbrains/plugins/scala/util/runners/RunWithAllIndexingModes.java @@ -0,0 +1,23 @@ +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; + +/** + * ATTENTION: this annotation should be used with care as it increases test execution time significantly. + *

+ * 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(); + }; + } +}