Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

file 196 lines (174 sloc) 9.367 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
package scala.tools.eclipse.launching

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 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 }
    val response = new Response[Tree]
    comp.askParsedEntered(source, keepLoaded = false, response)

    object JUnit4TestClasses extends JUnit4TestClassesCollector {
      val global: comp.type = comp
    }

    val trees = response.get.left.getOrElse(comp.EmptyTree)
    for {
      cdef <- JUnit4TestClasses.collect(trees)
      jdtElement <- comp.getJavaElement(cdef.symbol, scu.getJavaProject)
      jdtType <- jdtElement.asInstanceOfOpt[IType]
    } yield jdtType
  }()
}
Something went wrong with that request. Please try again.