Permalink
Browse files

Merge pull request #261 from dragos/issue/junit-test-finder

Implemented JUnit test finder logic in Scala.
  • Loading branch information...
2 parents 7317f0a + 50a7b2d commit 8cfd0795b736bcc1082595f578e345b02a1f29f4 @dragos dragos committed Dec 14, 2012
Showing with 723 additions and 100 deletions.
  1. +159 −0 org.scala-ide.sdt.core.tests/src/scala/tools/eclipse/launching/TestFinderTest.scala
  2. +8 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/.classpath
  3. +18 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/.project
  4. +7 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/non_src/NonSource.scala
  5. +6 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/TestInEmptyPackage.scala
  6. +8 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpacka/JTestA.java
  7. +9 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpacka/RunWithTest.java
  8. +8 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpackb/FakeJavaTest.java
  9. +4 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpackb/JTestB.java
  10. +1 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpackb/NonJavaFile.java_
  11. +11 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/packa/FakeTest.scala
  12. +17 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/packa/TestA.scala
  13. +10 −0 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/packb/TestB.scala
  14. +38 −4 org.scala-ide.sdt.core/plugin.xml
  15. +90 −0 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/JUnit4TestClassesCollector.scala
  16. +171 −95 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/JUnit4TestFinder.scala
  17. +116 −0 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/JavaJUnit4TestFinder.scala
  18. +36 −0 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/ScalaJUnitLaunchShortcut.scala
  19. +6 −1 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/ScalaLaunchableTester.scala
View
159 org.scala-ide.sdt.core.tests/src/scala/tools/eclipse/launching/TestFinderTest.scala
@@ -0,0 +1,159 @@
+package scala.tools.eclipse.launching
+
+import scala.tools.eclipse.testsetup.TestProjectSetup
+import org.junit.Test
+import org.eclipse.jdt.core.IType
+import org.eclipse.core.runtime.NullProgressMonitor
+import org.junit.Assert
+import org.eclipse.core.runtime.Path
+import collection.JavaConverters._
+import org.eclipse.core.resources.IResource
+import scala.collection.mutable
+
+object TestFinderTest extends TestProjectSetup("test-finder")
+
+class TestFinderTest {
+ import TestFinderTest._
+
+ @Test
+ def project_possibleMatches() {
+ val finder = new JUnit4TestFinder
+
+ val result = new mutable.HashSet[IType]
+ val possibleMatches = finder.filteredTestResources(project, project.javaProject, new NullProgressMonitor)
+ val expected = Set(getResource("/src/packa/TestA.scala"),
+ getResource("/src/packa/FakeTest.scala"),
+ getResource("/src/jpacka/JTestA.java"),
+ getResource("/src/jpacka/RunWithTest.java"),
+ getResource("/src/packb/TestB.scala"),
+ getResource("/src/jpackb/FakeJavaTest.java"),
+ getResource("/src/jpackb/JTestB.java"),
+ getResource("/src/TestInEmptyPackage.scala"))
+ Assert.assertEquals("wrong filtered files", expected, possibleMatches.toSet)
+ }
+
+ @Test
+ def scala_package_possibleMatches() {
+ val finder = new JUnit4TestFinder
+
+ val result = new mutable.HashSet[IType]
+ val possibleMatches = finder.filteredTestResources(project, project.javaProject.findPackageFragment("/test-finder/src/packa"), new NullProgressMonitor)
+ val expected = Set(getResource("/src/packa/TestA.scala"), getResource("/src/packa/FakeTest.scala"))
+ Assert.assertEquals("wrong filtered files", expected, possibleMatches.toSet)
+ }
+
+ @Test
+ def scala_package_matches() {
+ val finder = new JUnit4TestFinder
+
+ val result = new mutable.HashSet[IType]
+ finder.findTestsInContainer(project.javaProject.findPackageFragment("/test-finder/src/packa"), result, new NullProgressMonitor)
+
+ val expected = Set(getType("packa.TestA"), getType("packa.TestA1"))
+ Assert.assertEquals("wrong tests found", expected, result)
+ }
+
+ @Test
+ def java_package_matches() {
+ val finder = new JUnit4TestFinder
+
+ val result = new java.util.HashSet[IType]
+ finder.findTestsInContainer(project.javaProject.findPackageFragment("/test-finder/src/jpacka"), result, new NullProgressMonitor)
+
+ val expected = Set(getType("jpacka.JTestA"), getType("jpacka.RunWithTest"))
+ Assert.assertEquals("wrong tests found", expected, result.asScala.toSet)
+ }
+
+ @Test
+ def java_package_matches_inherited() {
+ val finder = new JUnit4TestFinder
+
+ val result = new java.util.HashSet[IType]
+ finder.findTestsInContainer(project.javaProject.findPackageFragment("/test-finder/src/jpackb"), result, new NullProgressMonitor)
+
+ val expected = Set(getType("jpackb.JTestB"))
+ Assert.assertEquals("wrong tests found", expected, result.asScala.toSet)
+ }
+
+ @Test
+ def project_matches() {
+ val finder = new JUnit4TestFinder
+
+ val result = new java.util.HashSet[IType]
+ finder.findTestsInContainer(project.javaProject, result, new NullProgressMonitor)
+
+ val expected = Set(
+ getType("jpacka.JTestA"),
+ getType("jpacka.RunWithTest"),
+ getType("jpackb.JTestB"),
+ getType("packa.TestA"),
+ getType("packa.TestA1"),
+ getType("packb.TestB"),
+ getType("TestInEmptyPackage"))
+ Assert.assertEquals("wrong tests found", expected, result.asScala.toSet)
+ }
+
+ @Test
+ def src_folder_matches() {
+ val finder = new JUnit4TestFinder
+
+ val result = new java.util.HashSet[IType]
+ finder.findTestsInContainer(project.javaProject.findPackageFragmentRoot("/test-finder/src"), result, new NullProgressMonitor)
+
+ val expected = Set(
+ getType("jpacka.JTestA"),
+ getType("jpacka.RunWithTest"),
+ getType("jpackb.JTestB"),
+ getType("packa.TestA"),
+ getType("packa.TestA1"),
+ getType("TestInEmptyPackage"),
+ getType("packb.TestB"))
+ Assert.assertEquals("wrong tests found", expected, result.asScala.toSet)
+ }
+
+ @Test
+ def only_one_type_element_matches() {
+ val finder = new JUnit4TestFinder
+
+ val result = new java.util.HashSet[IType]
+ val element = getType("packa.TestA")
+ finder.findTestsInContainer(element, result, new NullProgressMonitor)
+
+ // there are two types in the same source, we want only the one that we passed to the finder to be returned
+ val expected = Set(element)
+ Assert.assertEquals("wrong tests found", expected, result.asScala.toSet)
+ }
+
+ @Test
+ def empty_package_matches() {
+ val finder = new JUnit4TestFinder
+
+ val result = new java.util.HashSet[IType]
+ val element = getType("TestInEmptyPackage")
+ finder.findTestsInContainer(element, result, new NullProgressMonitor)
+
+ val expected = Set(element)
+ Assert.assertEquals("wrong tests found", expected, result.asScala.toSet)
+ }
+
+ @Test
+ def method_matches() {
+ val finder = new JUnit4TestFinder
+
+ val result = new java.util.HashSet[IType]
+ val testClass = getType("packa.TestA1")
+ val element = testClass.getMethod("testMethod1", Array())
+ finder.findTestsInContainer(element, result, new NullProgressMonitor)
+
+ val expected = Set(testClass)
+ Assert.assertEquals("wrong tests found", expected, result.asScala.toSet)
+ }
+
+ private def getType(fullyQualifiedName: String): IType =
+ project.javaProject.findType(fullyQualifiedName)
+
+ private def getResource(absolutePath: String): IResource =
+ project.underlying.findMember(absolutePath)
+
+ implicit def stringsArePaths(str: String): Path = new Path(str)
+}
View
8 org.scala-ide.sdt.core.tests/test-workspace/test-finder/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
View
18 org.scala-ide.sdt.core.tests/test-workspace/test-finder/.project
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>test-finder</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.scala-ide.sdt.core.scalabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.scala-ide.sdt.core.scalanature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
View
7 org.scala-ide.sdt.core.tests/test-workspace/test-finder/non_src/NonSource.scala
@@ -0,0 +1,7 @@
+// Scala file *not* on source path. Should not be considered for matches
+
+import org.junit.Test
+
+class Foo {
+ @Test def falseTest() {}
+}
View
6 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/TestInEmptyPackage.scala
@@ -0,0 +1,6 @@
+import org.junit.Test
+
+class TestInEmptyPackage {
+
+ @Test def foo() {}
+}
View
8 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpacka/JTestA.java
@@ -0,0 +1,8 @@
+package jpacka;
+
+import org.junit.Test;
+
+public class JTestA {
+ @Test public void testMethod1() {
+ }
+}
View
9 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpacka/RunWithTest.java
@@ -0,0 +1,9 @@
+package jpacka;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+public class RunWithTest {
+
+}
View
8 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpackb/FakeJavaTest.java
@@ -0,0 +1,8 @@
+package jpackb;
+
+/** This test is a fake one, as it mentions @Test but
+ * it is not on a method
+ */
+public class FakeJavaTest {
+
+}
View
4 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpackb/JTestB.java
@@ -0,0 +1,4 @@
+package jpackb;
+
+public class JTestB extends jpacka.JTestA {
+}
View
1 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/jpackb/NonJavaFile.java_
@@ -0,0 +1 @@
+// this is not a Java file
View
11 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/packa/FakeTest.scala
@@ -0,0 +1,11 @@
+package packa
+
+import org.junit.Test
+
+/**
+ * This class mentions @Test but it is not a test
+ * even though it might even import org.junit.Test
+ */
+class FakeTest {
+
+}
View
17 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/packa/TestA.scala
@@ -0,0 +1,17 @@
+package packa
+
+import org.junit.Test
+
+class TestA {
+
+ @Test
+ def testMethod() {}
+}
+
+class TestA1 {
+ @Test
+ def testMethod1() {}
+
+ @Test
+ def testMethod2() {}
+}
View
10 org.scala-ide.sdt.core.tests/test-workspace/test-finder/src/packb/TestB.scala
@@ -0,0 +1,10 @@
+package packb
+
+import org.junit.Test
+
+class TestB {
+
+ @Test
+ def testMethod1() {}
+
+}
View
42 org.scala-ide.sdt.core/plugin.xml
@@ -804,20 +804,20 @@
</contextualLaunch>
</shortcut>
<shortcut
- label="%JUnitShortcut.label"
+ label="Scala JUnit Test"
icon="$nl$/icons/full/obj16/julaunch.gif"
helpContextId="org.eclipse.jdt.junit.launch_shortcut"
- class="org.eclipse.jdt.junit.launcher.JUnitLaunchShortcut"
+ class="scala.tools.eclipse.launching.ScalaJUnitLaunchShortcut"
modes="run, debug"
- id="scala.tools.eclipse.scalatest.junitShortcut">
+ id="org.eclipse.shortcut.scala.junit">
<contextualLaunch>
<enablement>
<with variable="selection">
<count value="1"/>
<iterate>
<adapt type="org.eclipse.jdt.core.IJavaElement">
- <test property="org.eclipse.debug.ui.matchesPattern" value="*.scala"/>
<test property="org.eclipse.jdt.core.isInJavaProject"/>
+ <test property="org.eclipse.jdt.launching.hasProjectNature" args="org.scala-ide.sdt.core.scalanature"/>
<test property="org.eclipse.jdt.core.hasTypeOnClasspath" value="junit.framework.Test"/>
<test property="scala.tools.eclipse.launching.canLaunchAsJUnit" forcePluginActivation="true"/>
</adapt>
@@ -859,6 +859,16 @@
description="Shows inferred semicolons in the current editor"
categoryId="org.eclipse.ui.category.textEditor"
id="scala.tools.eclipse.toggleShowInferredSemicolonsAction"/>
+ <command
+ categoryId="scala.tools.eclipse.category"
+ description="Run Scala JUnit Test"
+ id="org.eclipse.shortcut.scala.junit.run"
+ name="Run Scala JUnit Test" />
+ <command
+ categoryId="scala.tools.eclipse.category"
+ description="Debug Scala JUnit Test"
+ id="org.eclipse.shortcut.scala.junit.debug"
+ name="Debug Scala JUnit Test" />
</extension>
<extension point="org.eclipse.ui.bindings">
@@ -870,6 +880,14 @@
commandId="org.eclipse.shortcut.scala.debug"
schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
sequence="M2+M3+D S" />
+ <key
+ commandId="org.eclipse.shortcut.scala.junit.run"
+ schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
+ sequence="M2+M3+T R" />
+ <key
+ commandId="org.eclipse.shortcut.scala.junit.debug"
+ schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
+ sequence="M2+M3+T D" />
</extension>
<extension point="org.scala-ide.sdt.core.sourcefileprovider">
@@ -1464,4 +1482,20 @@
</enablement>
</moveParticipant>
</extension>
+ <extension
+ point="org.eclipse.jdt.junit.internal_testKinds">
+ <!-- This extension point is copied from org.eclipse.jdt.junit.core, including the troubling questions. -->
+ <kind
+ displayName="Scala JUnit 4"
+ id="org.scala-ide.sdt.core.junit"
+ finderClass="scala.tools.eclipse.launching.JUnit4TestFinder"
+ loaderPluginId="org.eclipse.jdt.junit4.runtime"
+ loaderClass="org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader">
+ <runtimeClasspathEntry pluginId="org.eclipse.jdt.junit4.runtime" />
+ <!-- TODO: do we need these? -->
+ <runtimeClasspathEntry pluginId="org.eclipse.jdt.junit.core" />
+ <runtimeClasspathEntry pluginId="org.eclipse.jdt.junit.runtime"/>
+ <!-- END : do we need these? -->
+ </kind>
+ </extension>
</plugin>
View
90 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/JUnit4TestClassesCollector.scala
@@ -0,0 +1,90 @@
+package scala.tools.eclipse.launching
+
+import scala.collection.mutable.ListBuffer
+import scala.tools.eclipse.ScalaPresentationCompiler
+import scala.tools.eclipse.javaelements.ScalaSourceFile
+import scala.tools.eclipse.logging.HasLogger
+import scala.tools.nsc.MissingRequirementError
+import org.eclipse.jdt.core.IType
+
+/** Given the Scala AST of any compilation unit, traverse the AST and collect all top level class
+ * definitions that can be executed by the JUnit4 runtime.
+ *
+ * Note: This implementation assumes that the passed tree corresponds to the whole compilation unit.
+ * The reason is that it looks for top level class declarations to determine if there are
+ * any "runnable" JUnit test classes. This assumption is of course enforced in the code,
+ * have a look at the companion object.
+ */
+private[launching] abstract class JUnit4TestClassesCollector {
+ protected val global: ScalaPresentationCompiler
+
+ import global._
+
+ /** Collect all top level class definitions that can be executed by the JUnit4 runtime.
+ *
+ * @param tree The compilation unit's Scala AST.
+ */
+ def collect(tree: Tree): List[ClassDef] = {
+ val collector = new JUnit4TestClassesTraverser
+ collector.traverse(tree)
+ collector.hits.toList
+ }
+
+ /** Traverse the passed `Tree` and collect all class definitions that can be executed by the JUnit4 runtime. */
+ private class JUnit4TestClassesTraverser extends global.Traverser {
+ import JUnit4TestClassesTraverser._
+
+ val hits: ListBuffer[ClassDef] = ListBuffer.empty
+
+ override def traverse(tree: Tree): Unit = tree match {
+ case _: PackageDef => super.traverse(tree)
+ case cdef: ClassDef if isRunnableTestClass(cdef) => hits += cdef
+ case _ => ()
+ }
+
+ private def isRunnableTestClass(cdef: ClassDef): Boolean = {
+ global.askOption { () => isTopLevelClass(cdef) && isTestClass(cdef) } getOrElse false
+ }
+
+ private def isTopLevelClass(cdef: ClassDef): Boolean = {
+ def isConcreteClass: Boolean = {
+ val csym = cdef.symbol
+ // Abstract classes cannot be run by definition, so they should be ignored.
+ // And trait do have the `isClass` member set to `true`, so we need to check `isTrait` as well.
+ csym.isClass && !csym.isAbstractClass && !csym.isTrait
+ }
+ isConcreteClass && cdef.symbol.owner.isPackageClass
+ }
+
+ private def isTestClass(cdef: ClassDef): Boolean = hasRunWithAnnotation(cdef) || hasJUnitTestMethod(cdef)
+
+ private def hasRunWithAnnotation(cdef: ClassDef): Boolean = RunWithAnnotationOpt exists { runWithAnn =>
+ cdef.symbol.info.baseClasses exists { hasAnnotation(_, runWithAnn) }
+ }
+
+ private def hasJUnitTestMethod(cdef: ClassDef): Boolean = TestAnnotationOpt exists { ta =>
+ cdef.symbol.info.members.exists { hasAnnotation(_, ta) }
+ }
+
+ private def hasAnnotation(member: Symbol, ann: Symbol): Boolean = {
+ if (!member.isInitialized) member.initialize
+ member.hasAnnotation(ann)
+ }
+ }
+
+ private object JUnit4TestClassesTraverser extends HasLogger {
+ private lazy val TestAnnotationOpt = getClassSafe("org.junit.Test")
+ private lazy val RunWithAnnotationOpt = getClassSafe("org.junit.runner.RunWith")
+
+ /** Don't crash if the class is not on the classpath. */
+ private def getClassSafe(fullName: String): Option[Symbol] = {
+ try {
+ Option(definitions.getClass(newTypeName(fullName)))
+ } catch {
+ case _: MissingRequirementError =>
+ logger.info("Type `" + fullName + "` is not available in the project's classpath.")
+ None
+ }
+ }
+ }
+}
View
266 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/JUnit4TestFinder.scala
@@ -1,23 +1,181 @@
package scala.tools.eclipse.launching
-import scala.collection.mutable.ListBuffer
-import scala.tools.eclipse.ScalaPresentationCompiler
-import scala.tools.eclipse.javaelements.ScalaSourceFile
-import scala.tools.eclipse.logging.HasLogger
-import scala.tools.nsc.MissingRequirementError
+import collection.mutable
+import org.eclipse.jdt.core.IJavaElement
+import org.eclipse.core.runtime.IProgressMonitor
+import java.util.{ Set => JSet }
import org.eclipse.jdt.core.IType
+import scala.tools.eclipse.ScalaPlugin
+import scala.tools.eclipse.javaelements.ScalaElement
+import org.eclipse.jdt.core.IPackageFragment
+import org.eclipse.core.resources.IProject
+import org.eclipse.jdt.internal.core.JavaProject
+import org.eclipse.search.core.text.TextSearchScope
+import org.eclipse.core.resources.IResource
+import org.eclipse.search.core.text.TextSearchEngine
+import org.eclipse.search.core.text.TextSearchRequestor
+import org.eclipse.core.resources.IFile
+import org.eclipse.search.core.text.TextSearchMatchAccess
+import org.eclipse.jdt.core.IJavaProject
+import org.eclipse.jdt.core.JavaCore
+import scala.collection.JavaConverters._
+import org.eclipse.core.runtime.NullProgressMonitor
+import org.eclipse.jdt.internal.junit.JUnitMessages
+import org.eclipse.core.runtime.SubProgressMonitor
+import org.eclipse.core.runtime.SubMonitor
+import org.eclipse.jdt.internal.junit.launcher.ITestFinder
+import scala.tools.eclipse.ScalaProject
+import scala.tools.eclipse.javaelements.ScalaSourceFile
import scala.tools.eclipse.util.Utils.any2optionable
+import org.eclipse.jdt.core.IMember
+import org.eclipse.jdt.core.IPackageFragmentRoot
+import scala.tools.eclipse.logging.HasLogger
+import org.eclipse.jdt.core.IParent
+
+/** A JUnit4 test finder that works for Java and Scala.
+ *
+ * We hook this test finder using an internal extension point defined by `org.eclipse.jdt.junit.core`,
+ * `internal_testkinds`. This class is used both when right-clicking on a Java element and choosing
+ * "Run As - Scala JUnit test" and when hitting the "Search" button in the JUnit Run configuration dialog.
+ *
+ * Alternatives were considered, but not pursued:
+ * - AspectJ to hook into the JUnit runner. Discarded as hackish, not future-proof and the risk of breaking
+ * the plain JDT plugin
+ * - implement a Scala JUnit configuration type, but that would duplicate more of the JUnit configuration type
+ * and involved rewriting dialogs and buttons
+ *
+ * We needed to work around the lack of proper Scala search, and do a best-effort search. Known limitations:
+ *
+ * - only @Test and @RunWith-style tests are recognized
+ * - classes that *don't* define any @Test members, but inherit them, are not found in Scala sources
+ * - this can be worked around by adding `@Test` anywhere in the file, for instance as a comment
+ */
+class JUnit4TestFinder extends ITestFinder with HasLogger {
+ import JUnit4TestFinder._
+
+ override def findTestsInContainer(element: IJavaElement, result: JSet[_], pm: IProgressMonitor): Unit =
+ findTestsInContainer(element, result.asInstanceOf[JSet[IType]].asScala, pm)
+
+ override def isTest(tpe: IType): Boolean = {
+ ScalaLaunchShortcut.getJunitTestClasses(tpe).nonEmpty
+ }
+
+ /** Find all JUnit 4 tests under `element` and put them in `result`.
+ *
+ * `element` can be any Java element: a project, a package, a source folder, a source file, a type.
+ */
+ def findTestsInContainer(element: IJavaElement, result: mutable.Set[IType], pm: IProgressMonitor): Unit = element match {
+ case _: ScalaSourceFile =>
+ result ++= ScalaLaunchShortcut.getJunitTestClasses(element)
+ case tpe: IType =>
+ result ++= ScalaLaunchShortcut.getJunitTestClasses(element).filter(_ == tpe)
+ case member: IMember =>
+ val parent = member.getAncestor(IJavaElement.TYPE)
+ result ++= ScalaLaunchShortcut.getJunitTestClasses(element).filter(_ == parent)
+ case _: IProject | _: IJavaProject | _: IPackageFragment | _: IPackageFragmentRoot =>
+ findTestsInContainer1(element, result, pm)
+ case _ =>
+ logger.info("Unknown element type when looking for tests: %s:%s".format(element.getClass(), element.toString))
+ ()
+ }
+
+ /** This method finds tests in any container, but may be imprecise if `element` is smaller than a source file.
+ *
+ * This method filters out all source file in a first step, and then delegates to `JUnit4TestFinder.findTestClasses(scu)`
+ * for each potential match. The filtering is based on a textual search, and misses test files that don't mention
+ * at all any of the test annotations (for instance, by inheriting all their tests).
+ */
+ private def findTestsInContainer1(element: IJavaElement, result: mutable.Set[IType], _pm: IProgressMonitor): Unit = {
+ val pm = if (_pm == null) new NullProgressMonitor else _pm
+ val scalaProject = ScalaPlugin.plugin.asScalaProject(element.getJavaProject().getProject()).get // we know it's a Scala project or we wouldn't be here
+
+ val progress = SubMonitor.convert(pm, JUnitMessages.JUnit4TestFinder_searching_description, 4)
+ try {
+ val (scalaCandidates, javaCandidates) = filteredTestResources(scalaProject, element, progress.newChild(2)).partition(_.getFileExtension == "scala")
+
+ result ++= scalaMatches(scalaCandidates, progress.newChild(1))
+ result ++= ((new JavaJUnit4TestFinder).javaMatches(scalaProject.javaProject, javaCandidates, progress.newChild(1)))
+ } finally
+ pm.done()
+ }
+
+ private[launching] def filteredTestResources(prj: ScalaProject, element: IJavaElement, progress: IProgressMonitor): Seq[IResource] = {
+ val candidates = element match {
+ case project: IJavaProject => prj.allSourceFiles.toSeq
+ case _ => Seq(element.getResource)
+ }
+ progress.worked(1)
+
+ likelyTestResources(candidates, progress)
+ }
+
+ private def likelyTestResources(roots: Seq[IResource], _pm: IProgressMonitor): Seq[IResource] = {
+ val pm = SubMonitor.convert(_pm, "Textual search for likely sources that contain tests", roots.size)
+ val scope = TextSearchScope.newSearchScope(roots.toArray, FILE_NAME_PATTERN.pattern, /* visitDerivedResoures = */ false)
+
+ if (pm.isCanceled()) Seq()
+ else {
+ val engine = TextSearchEngine.createDefault()
+ val req = new PotentialTestFilesCollector(pm)
+ engine.search(scope, req, TEST_PATTERN.pattern, pm)
+ pm.done()
+ req.files
+ }
+ }
+
+ private def scalaMatches(candidates: Seq[IResource], _pm: IProgressMonitor): Seq[IType] = {
+ import scala.util.control.Exception._
+
+ val pm = SubMonitor.convert(_pm, "Locating Scala Test matches", candidates.size)
+ for {
+ resource <- candidates
+ _ = pm.worked(1) // interpose a side-effect
+ element <- Option(JavaCore.create(resource)).toSeq
+ if !pm.isCanceled()
+ tpe <- allCatch.withApply(_ => Seq()) { ScalaLaunchShortcut.getJunitTestClasses(element) }
+ } yield tpe
+ }
+
+ /** Collect all files where matches occur. Optimizations:
+ *
+ * - accept only one match per file, tell the engine to skip the rest
+ * - all Java files are collected, regardless whether they match or not the search pattern
+ * (this is because they are potential candidates, and JDT search may find inherited tests)
+ */
+ private class PotentialTestFilesCollector(pm: IProgressMonitor) extends TextSearchRequestor {
+ val files = mutable.ListBuffer[IFile]()
+
+ override def acceptFile(file: IFile): Boolean = {
+ pm.worked(1)
+ if (file.getFileExtension() == "java") {
+ files += file // all Java files, with or without matches, are likely candidates (because JDT search works :D)
+ false
+ } else
+ true // for Scala files, we want to continue and get real match reports
+ }
+
+ override def acceptPatternMatch(matchAccess: TextSearchMatchAccess): Boolean = {
+ files += matchAccess.getFile()
+ false // don't report more matches in this file
+ }
+ }
+}
/** Given a Scala compilation unit, finds all top level class definition that can be run as JUnit4 test classes.
- *
- * If a source contains error, the `JUnit4TestFinder` will likely still be able to find executable JUnit test
- * classes in the passed source. If we wanted to be smart, we could check if the passed source (or even the
- * enclosing project) has any compile-time error, and return an empty set of runnable test classes.
- * However, this turns out to be a bad idea, because the user may not understand that the reason why it can't run
- * the test class is because he has to fix all compilation error. Therefore, it is better to always return the
- * set of executable JUnit4 test classes and let the user figure out the cause why the test class cannot be run.
- */
+ *
+ * If a source contains errors, the `JUnit4TestFinder` will likely still be able to find executable JUnit test
+ * classes in the passed source. If we wanted to be smart, we could check if the passed source (or even the
+ * enclosing project) has any compile-time error, and return an empty set of runnable test classes.
+ * However, this turns out to be a bad idea, because the user may not understand that the reason why it can't run
+ * the test class is because he has to fix all compilation errors. Therefore, it is better to always return the
+ * set of executable JUnit4 test classes and let the user figure out the cause why the test class cannot be run.
+ */
object JUnit4TestFinder {
+ private val MARKER_STRINGS = Set("@Test", "@RunWith")
+
+ /** Textual filter, used to find candidate resources for JUnit tests. */
+ private val TEST_PATTERN = MARKER_STRINGS.mkString("|").r
+ private val FILE_NAME_PATTERN = """(.*\.java$)|(.*\.scala$)""".r
def findTestClasses(scu: ScalaSourceFile): List[IType] = scu.withSourceFile { (source, comp) =>
import comp.{ ClassDef, Response, Tree }
@@ -36,85 +194,3 @@ object JUnit4TestFinder {
} yield jdtType
}()
}
-
-/** Given the Scala AST of any compilation unit, traverse the AST and collect all top level class
- * definitions that can be executed by the JUnit4 runtime.
- *
- * Note: This implementation assumes that the passed tree corresponds to the whole compilation unit.
- * The reason is that it looks for top level class declaration to determine if there are
- * any "runnable" JUnit test classes. This assumption is of course enforced in the code,
- * have a look at the companion object.
- */
-private abstract class JUnit4TestClassesCollector {
- protected val global: ScalaPresentationCompiler
-
- import global._
-
- /** Collect all top level class definitions that can be executed by the JUnit4 runtime.
- *
- * @param tree The compilation unit's Scala AST.
- */
- def collect(tree: Tree): List[ClassDef] = {
- val collector = new JUnit4TestClassesTraverser
- collector.traverse(tree)
- collector.hits.toList
- }
-
- /** Traverse the passed `Tree` and collect all class definitions that can be executed by the JUnit4 runtime. */
- private class JUnit4TestClassesTraverser extends global.Traverser {
- import JUnit4TestClassesTraverser._
-
- val hits: ListBuffer[ClassDef] = ListBuffer.empty
-
- override def traverse(tree: Tree): Unit = tree match {
- case _: PackageDef => super.traverse(tree)
- case cdef: ClassDef if isRunnableTestClass(cdef) => hits += cdef
- case _ => ()
- }
-
- private def isRunnableTestClass(cdef: ClassDef): Boolean = {
- global.askOption { () => isTopLevelClass(cdef) && isTestClass(cdef) } getOrElse false
- }
-
- private def isTopLevelClass(cdef: ClassDef): Boolean = {
- def isConcreteClass: Boolean = {
- val csym = cdef.symbol
- // Abstract classes cannot be run by definition, so they should be ignored.
- // And trait do have the `isClass` member set to `true`, so we need to check `isTrait` as well.
- csym.isClass && !csym.isAbstractClass && !csym.isTrait
- }
- isConcreteClass && cdef.symbol.owner.isPackageClass
- }
-
- private def isTestClass(cdef: ClassDef): Boolean = hasRunWithAnnotation(cdef) || hasJUnitTestMethod(cdef)
-
- private def hasRunWithAnnotation(cdef: ClassDef): Boolean = RunWithAnnotationOpt exists { runWithAnn =>
- cdef.symbol.info.baseClasses exists { hasAnnotation(_, runWithAnn) }
- }
-
- private def hasJUnitTestMethod(cdef: ClassDef): Boolean = TestAnnotationOpt exists { ta =>
- cdef.symbol.info.members.exists { hasAnnotation(_, ta) }
- }
-
- private def hasAnnotation(member: Symbol, ann: Symbol): Boolean = {
- if (!member.isInitialized) member.initialize
- member.hasAnnotation(ann)
- }
- }
-
- private object JUnit4TestClassesTraverser extends HasLogger {
- private lazy val TestAnnotationOpt = getClassSafe("org.junit.Test")
- private lazy val RunWithAnnotationOpt = getClassSafe("org.junit.runner.RunWith")
-
- /** Don't crash if the class is not on the classpath. */
- private def getClassSafe(fullName: String): Option[Symbol] = {
- try {
- Option(definitions.getClass(newTypeName(fullName)))
- } catch {
- case _: MissingRequirementError =>
- logger.info("Type `" + fullName + "` is not available in the project's classpath.")
- None
- }
- }
- }
-}
View
116 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/JavaJUnit4TestFinder.scala
@@ -0,0 +1,116 @@
+package scala.tools.eclipse.launching
+
+import scala.collection.mutable
+import org.eclipse.core.runtime.SubProgressMonitor
+import org.eclipse.jdt.core.search._
+import org.eclipse.core.resources.IResource
+import org.eclipse.core.runtime.IProgressMonitor
+import org.eclipse.jdt.core._
+import org.eclipse.jdt.internal.junit.util.CoreTestSearchEngine
+import org.eclipse.jdt.internal.junit.JUnitCorePlugin
+import org.eclipse.jdt.core.dom.IAnnotationBinding
+import org.eclipse.jdt.core.dom.ITypeBinding
+import scala.annotation.tailrec
+import scala.tools.eclipse.logging.HasLogger
+
+/** This class re-implements the logic of {{{org.eclipse.jdt.internal.junit.launcher.JUnit4TestFinder}}}, with one
+ * limitation:
+ *
+ * - no JUnit 3.8 support (extending `TestCase`) or `suite()` methods. The original code is commented out in
+ * method `javaMatches`.
+ *
+ * The original code organization is preserved, but Scala-fied.
+ */
+class JavaJUnit4TestFinder extends HasLogger {
+ import JavaJUnit4TestFinder._
+
+ /** Return all Java JUnit 4 tests found in the given sources. */
+ def javaMatches(javaProject: IJavaProject, sources: Seq[IResource], pm: IProgressMonitor): Seq[IType] = try {
+ val region = JavaCore.newRegion()
+ sources.foreach(res => Option(JavaCore.create(res)).map(region.add))
+
+ val hierarchy = JavaCore.newTypeHierarchy(region, null, new SubProgressMonitor(pm, 1))
+ val allClasses = hierarchy.getAllClasses()
+
+ // search for all types with references to RunWith and Test and all subclasses
+ val candidates = new mutable.HashSet[IType]
+ val result = new mutable.HashSet[IType]
+ val requestor = new AnnotationSearchRequestor(hierarchy, candidates)
+
+ val scope = SearchEngine.createJavaSearchScope(allClasses.asInstanceOf[Array[IJavaElement]], IJavaSearchScope.SOURCES)
+ val matchRule = SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE
+ val runWithPattern = SearchPattern.createPattern(RUN_WITH.name, IJavaSearchConstants.ANNOTATION_TYPE, IJavaSearchConstants.ANNOTATION_TYPE_REFERENCE, matchRule)
+ val testPattern = SearchPattern.createPattern(TEST.name, IJavaSearchConstants.ANNOTATION_TYPE, IJavaSearchConstants.ANNOTATION_TYPE_REFERENCE, matchRule)
+
+ val annotationsPattern = SearchPattern.createOrPattern(runWithPattern, testPattern)
+ val searchParticipants = Array(SearchEngine.getDefaultSearchParticipant())
+ new SearchEngine().search(annotationsPattern, searchParticipants, scope, requestor, new SubProgressMonitor(pm, 2))
+
+ // find all classes in the region
+ for (curr <- candidates) {
+ if (CoreTestSearchEngine.isAccessibleClass(curr) && !Flags.isAbstract(curr.getFlags()) && region.contains(curr)) {
+ result.add(curr)
+ }
+ }
+
+ // add all classes implementing JUnit 3.8's Test interface in the region
+ // val testInterface = javaProject.findType(JUnitCorePlugin.TEST_INTERFACE_NAME)
+ // if (testInterface != null) {
+ // CoreTestSearchEngine.findTestImplementorClasses(hierarchy, testInterface, region, result);
+ // }
+
+ //JUnit 4.3 can also run JUnit-3.8-style public static Test suite() methods:
+ // CoreTestSearchEngine.findSuiteMethods(element, result, new SubProgressMonitor(pm, 1));
+ result.toSeq
+ } catch {
+ case e: Exception =>
+ logger.info("Java Test Finder crashed while looking for Java matches.", e)
+ Seq()
+ } finally {
+ pm.done()
+ }
+
+ private class AnnotationSearchRequestor(hierarchy: ITypeHierarchy, result: mutable.Set[IType]) extends SearchRequestor {
+ def acceptSearchMatch(smatch: SearchMatch) {
+ if (smatch.getAccuracy() == SearchMatch.A_ACCURATE && !smatch.isInsideDocComment()) {
+ smatch.getElement() match {
+ case tpe: IType => addTypeAndSubtypes(tpe)
+ case meth: IMethod => addTypeAndSubtypes(meth.getDeclaringType())
+ }
+ }
+ }
+
+ private def addTypeAndSubtypes(tpe: IType) {
+ if (result.add(tpe))
+ hierarchy.getSubclasses(tpe).foreach(addTypeAndSubtypes)
+ }
+ }
+}
+
+object JavaJUnit4TestFinder {
+ private final val RUN_WITH = new Annotation("org.junit.runner.RunWith")
+ private final val TEST = new Annotation("org.junit.Test")
+
+ private class Annotation(val name: String) {
+ private def annotates(annotations: Array[IAnnotationBinding]): Boolean = {
+ annotations exists { annot =>
+ val annotationType = annot.getAnnotationType()
+ (annotationType != null && (annotationType.getQualifiedName().equals(name)))
+ }
+ }
+
+ @tailrec
+ private def annotatesTypeOrSuperTypes(tpe: ITypeBinding): Boolean = {
+ (tpe != null) &&
+ (annotates(tpe.getAnnotations)
+ || annotatesTypeOrSuperTypes(tpe.getSuperclass))
+ }
+
+ @tailrec
+ private def annotatesAtLeastOneMethod(tpe: ITypeBinding): Boolean = {
+ (tpe != null) &&
+ (tpe.getDeclaredMethods.exists(m => annotates(m.getAnnotations))
+ || annotatesAtLeastOneMethod(tpe.getSuperclass))
+ }
+ }
+}
View
36 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/ScalaJUnitLaunchShortcut.scala
@@ -0,0 +1,36 @@
+package scala.tools.eclipse.launching
+
+import org.eclipse.jdt.junit.launcher.JUnitLaunchShortcut
+import org.eclipse.jdt.core.IJavaElement
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy
+import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants
+
+/** A `Run As Scala JUnit Test` shortcut. The only thing that we need to change compared to
+ * the plain Java JUnit shortcut is the test runner kind. We introduced a new test kind,
+ * similar to the JDT 'JUnit4' and 'JUnit3' test kinds, whose sole responsibility is to
+ * locate tests.
+ *
+ * @see the `internal_testkinds` extension point.
+ *
+ */
+class ScalaJUnitLaunchShortcut extends JUnitLaunchShortcut {
+
+ /** Add the Scala JUnit test kind to the configuration.. */
+ override def createLaunchConfiguration(element: IJavaElement): ILaunchConfigurationWorkingCopy = {
+ val conf = super.createLaunchConfiguration(element)
+
+ conf.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND, ScalaJUnitLaunchShortcut.SCALA_JUNIT_TEST_KIND)
+ conf
+ }
+
+ /** We need to force the creation of a new launch configuration if the test kind is different, otherwise
+ * the plain JDT test finder would be run, and possibly miss tests.
+ */
+ override def getAttributeNamesToCompare(): Array[String] = {
+ super.getAttributeNamesToCompare() :+ JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND
+ }
+}
+
+object ScalaJUnitLaunchShortcut {
+ final val SCALA_JUNIT_TEST_KIND = "org.scala-ide.sdt.core.junit"
+}
View
7 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/ScalaLaunchableTester.scala
@@ -20,6 +20,7 @@ import org.eclipse.jdt.core.IMethod
import org.eclipse.jdt.core.IType
import org.eclipse.jdt.core.JavaModelException
import scala.tools.eclipse.util.EclipseUtils._
+import scala.tools.eclipse.javaelements.ScalaSourceFile
class ScalaLaunchableTester extends PropertyTester {
/**
@@ -55,7 +56,11 @@ class ScalaLaunchableTester extends PropertyTester {
*/
private def canLaunchAsJUnit(element: IJavaElement): Boolean = {
try {
- ScalaLaunchShortcut.getJunitTestClasses(element).length > 0
+ element match {
+ case e: ScalaSourceFile =>
+ ScalaLaunchShortcut.getJunitTestClasses(element).nonEmpty
+ case _ => true
+ }
} catch {
case e: JavaModelException => false
case e: CoreException => false

0 comments on commit 8cfd079

Please sign in to comment.