mirror of https://github.com/sbt/sbt.git
Support for tests written in Java and annotation-based test frameworks
This commit is contained in:
parent
ab2e038817
commit
77567b6ad3
|
|
@ -20,7 +20,7 @@ object Analyzer
|
|||
final class Analyzer(val global: Global, val callback: AnalysisCallback) extends NotNull
|
||||
{
|
||||
import global._
|
||||
import Compat.{archive, linkedClass, nameString}
|
||||
import Compat.{archive, hasAnnotation, linkedClass, nameString}
|
||||
|
||||
def newPhase(prev: Phase): Phase = new AnalyzerPhase(prev)
|
||||
private class AnalyzerPhase(prev: Phase) extends Phase(prev)
|
||||
|
|
@ -31,6 +31,8 @@ final class Analyzer(val global: Global, val callback: AnalysisCallback) extends
|
|||
{
|
||||
val outputDirectory = new File(global.settings.outdir.value)
|
||||
val superclasses = callback.superclassNames flatMap(classForName)
|
||||
val annotations = callback.annotationNames flatMap (classForName) map { sym => (nameString(sym), sym) }
|
||||
def annotated(sym: Symbol): Iterable[String] = annotatedClass(sym, annotations)
|
||||
|
||||
for(unit <- currentRun.units)
|
||||
{
|
||||
|
|
@ -63,15 +65,19 @@ final class Analyzer(val global: Global, val callback: AnalysisCallback) extends
|
|||
// find subclasses and modules with main methods
|
||||
for(clazz @ ClassDef(mods, n, _, _) <- unit.body)
|
||||
{
|
||||
// for each annotation on the class, if its name is in annotationNames, callback.foundAnnotated(sourceFile, nameString(sym), annotationName, isModule)
|
||||
val sym = clazz.symbol
|
||||
if(sym != NoSymbol && mods.isPublic && !mods.isAbstract && !mods.isTrait &&
|
||||
!sym.isImplClass && sym.isStatic && !sym.isNestedClass)
|
||||
{
|
||||
val name = nameString(sym)
|
||||
val isModule = sym.isModuleClass
|
||||
for(superclass <- superclasses.filter(sym.isSubClass))
|
||||
callback.foundSubclass(sourceFile, nameString(sym), nameString(superclass), isModule)
|
||||
callback.foundSubclass(sourceFile, name, nameString(superclass), isModule)
|
||||
if(isModule && hasMainMethod(sym))
|
||||
callback.foundApplication(sourceFile, nameString(sym))
|
||||
callback.foundApplication(sourceFile, name)
|
||||
for(annotation <- annotated(sym))
|
||||
callback.foundAnnotated(sourceFile, name, annotation, isModule)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +136,10 @@ final class Analyzer(val global: Global, val callback: AnalysisCallback) extends
|
|||
None
|
||||
}
|
||||
}
|
||||
private def annotated(annotations: Iterable[(String, Symbol)])(sym: Symbol): Iterable[String] =
|
||||
annotations flatMap { case (name, ann) => if(hasAnnotation(sym)(ann)) name :: Nil else Nil }
|
||||
private def annotatedClass(sym: Symbol, annotations: Iterable[(String, Symbol)]): Iterable[String] =
|
||||
if(annotations.isEmpty) Nil else annotated(annotations)(sym) ++ sym.info.nonPrivateMembers.flatMap { annotated(annotations) }
|
||||
|
||||
// doesn't seem to be in 2.7.7, so copied from GenJVM to here
|
||||
private def moduleSuffix(sym: Symbol) =
|
||||
|
|
@ -223,7 +233,12 @@ final class Analyzer(val global: Global, val callback: AnalysisCallback) extends
|
|||
def fullNameString(sep: Char) = s.fullName(sep); def fullName(sep: Char) = sourceCompatibilityOnly
|
||||
|
||||
def linkedClassOfModule = s.companionClass; def companionClass = sourceCompatibilityOnly
|
||||
// In 2.8, hasAttribute is renamed to hasAnnotation
|
||||
def hasAnnotation(a: Symbol) = s.hasAttribute(a); def hasAttribute(a: Symbol) = sourceCompatibilityOnly
|
||||
}
|
||||
|
||||
def hasAnnotation(s: Symbol)(ann: Symbol) = atPhase(currentRun.typerPhase) { s.hasAnnotation(ann) }
|
||||
|
||||
/** After 2.8.0.Beta1, getArchive was renamed archive.*/
|
||||
private implicit def zipCompat(z: ZipArchive#Entry): ZipCompat = new ZipCompat(z)
|
||||
private final class ZipCompat(z: ZipArchive#Entry)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ public interface AnalysisCallback
|
|||
{
|
||||
/** The names of classes that the analyzer should find subclasses of.*/
|
||||
public String[] superclassNames();
|
||||
/** The names of annotations that the analyzer should look for on methods and classes.*/
|
||||
public String[] annotationNames();
|
||||
/** Called when the the given superclass could not be found on the classpath by the compiler.*/
|
||||
public void superclassNotFound(String superclassName);
|
||||
/** Called before the source at the given location is processed. */
|
||||
|
|
@ -16,6 +18,8 @@ public interface AnalysisCallback
|
|||
/** Called when the a subclass of one of the classes given in <code>superclassNames</code> is
|
||||
* discovered.*/
|
||||
public void foundSubclass(File source, String subclassName, String superclassName, boolean isModule);
|
||||
/** Called when an annotation with name <code>annotationName</code> is found on a class or one of its methods.*/
|
||||
public void foundAnnotated(File source, String className, String annotationName, boolean isModule);
|
||||
/** Called to indicate that the source file <code>source</code> depends on the source file
|
||||
* <code>dependsOn</code>. Note that only source files included in the current compilation will
|
||||
* passed to this method. Dependencies on classes generated by sources not in the current compilation will
|
||||
|
|
|
|||
43
notes
43
notes
|
|
@ -1,19 +1,34 @@
|
|||
DONE
|
||||
- fixed Jetty 7 and JRebel
|
||||
- fixed make-pom and missing <repositories> tag
|
||||
- fixed referencing xsbti classes from project definition and plugins
|
||||
- fixed java + scala.compiler.jar not on classpath
|
||||
- Java tests + Annotation detection (described below)
|
||||
|
||||
Java tests + Annotation detection:
|
||||
- Discovered is new root of hierarchy representing discovered subclasses + annotations. TestDefinition no longer fulfills this role.
|
||||
- TestDefinition is modified to be name+Fingerprint and represents a runnable test. It need not be Discovered.
|
||||
- added foundAnnotation to AnalysisCallback
|
||||
- added Runner2, Fingerprint, AnnotationFingerprint, SubclassFingerprint to test-interface. Existing test frameworks should still work. Implement Runner2 from now on.
|
||||
- replaced testDefinitionClassNames with testFingerprints in CompileConfiguration. needs to be renamed again to something like 'discovery', at least dropping 'test'
|
||||
- detect subclasses and annotations in Java sources (really, their class files)
|
||||
- test frameworks can now specify annotation fingerprints. They specify the names of annotations and sbt discovers classes with the annotations on it or one of its methods.
|
||||
|
||||
- script tasks (in 'scripts' branch). To use:
|
||||
1) add implementation of jsr223 to project/build/lib, declare in project/plugins, or add to sbt startup classpath
|
||||
2) Mix sbt.scripts.Scripts into your project definition
|
||||
3) Create project/scripts/name.ext
|
||||
4) Run as 'name'
|
||||
5) To work with dependencies, get the task from the scriptedTasks map by name. Ex:
|
||||
lazy val myTask = task { ... } dependsOn scriptedTasks("myscript").
|
||||
|
||||
- added experimental 'sbt-update version [output.jar]' command enabled by mixing in UpdateSbt trait. It probably doesn't work on Windows, but confirmation either way would be helpful.
|
||||
Changes 'sbt.version' after verifying the version is valid by downloading that version of sbt.
|
||||
Updates the current launcher jar or the location specified in the second argument with either a downloaded sbt-launch-<version>.jar if available or a copy of the current launcher jar that uses <version> by default when starting new projects.
|
||||
|
||||
Goals/Guidelines for xsbt
|
||||
=====
|
||||
As usual:
|
||||
- Immutable interfaces
|
||||
- Typesafe
|
||||
- Robust, flexible API
|
||||
|
||||
TODO
|
||||
TODO:
|
||||
- tasks as promises
|
||||
- aggressive recompilation
|
||||
- API API. Read, write, search API.
|
||||
- source build system
|
||||
|
||||
- launcher interface versioning
|
||||
- allow comments in datatype definition file
|
||||
- tests for API extractor
|
||||
|
|
@ -28,6 +43,8 @@ Task engine
|
|||
+ log each task to a file
|
||||
+ interactive control over logging (might require tasks that use console to be explicit in their dependence on the console)
|
||||
|
||||
- should be able to connect one preprocessor to another
|
||||
|
||||
- Interfaces subproject that depends on no other project and defines interfaces in package xsbti. They are written in Java and cannot refer to Scala classes (compileOrder = JavaThenScala). These interfaces can be used to pass objects across ClassLoader boundaries.
|
||||
- launcher/main interface is not static (no system properties should be used past the static entry point)
|
||||
- simple, well-defined ClassLoaders
|
||||
|
|
@ -45,6 +62,4 @@ Dependency Management
|
|||
- configurations completely defined within project (use ModuleConfiguration.configurations)
|
||||
|
||||
Launcher
|
||||
- see launch.specification
|
||||
- boot interface contains static final int version = N that main xsbt can use to check if it can be loaded by that version (a lower bound check)
|
||||
- main xsbt has static final int version = N that boot can use to check if it can load that version (a lower bound check)
|
||||
- see launch.specification
|
||||
|
|
@ -44,7 +44,7 @@ abstract class SbtProject(info: ProjectInfo) extends DefaultProject(info) with t
|
|||
val jetty7server = "org.eclipse.jetty" % "jetty-server" % "7.0.1.v20091125" % "optional"
|
||||
val jetty7webapp = "org.eclipse.jetty" % "jetty-webapp" % "7.0.1.v20091125" % "optional"
|
||||
|
||||
val testInterface = "org.scala-tools.testing" % "test-interface" % "0.4"
|
||||
val testInterface = "org.scala-tools.testing" % "test-interface" % "0.5"
|
||||
|
||||
def deepSources = Path.finder { topologicalSort.flatMap { case p: ScalaPaths => p.mainSources.getFiles } }
|
||||
lazy val sbtDoc = scaladocTask("sbt", deepSources, docPath, docClasspath, documentOptions)
|
||||
|
|
|
|||
|
|
@ -201,24 +201,24 @@ private[sbt] final class BuilderCompileAnalysis(analysisPath: Path, projectPath:
|
|||
}
|
||||
class CompileAnalysis(analysisPath: Path, projectPath: Path, log: Logger) extends BasicCompileAnalysis(analysisPath, projectPath, log)
|
||||
{
|
||||
private val testMap = new HashMap[Path, Set[TestDefinition]]
|
||||
private val testMap = new HashMap[Path, Set[Discovered]]
|
||||
private val applicationsMap = new HashMap[Path, Set[String]]
|
||||
def allTests = all(testMap)
|
||||
def allApplications = all(applicationsMap)
|
||||
def addTest(source: Path, test: TestDefinition) = add(source, test, testMap)
|
||||
def addTest(source: Path, test: Discovered) = add(source, test, testMap)
|
||||
def addApplication(source: Path, className: String) = add(source, className, applicationsMap)
|
||||
|
||||
def testSourceMap: Map[String, Path] =
|
||||
{
|
||||
val map = new HashMap[String, Path]
|
||||
for( (source, tests) <- testMap; test <- tests) map(test.testClassName) = source
|
||||
for( (source, tests) <- testMap; test <- tests) map(test.className) = source
|
||||
map
|
||||
}
|
||||
|
||||
override protected def mapsToClear = applicationsMap :: testMap :: super.mapsToClear
|
||||
override protected def mapsToRemoveSource = applicationsMap :: testMap :: super.mapsToRemoveSource
|
||||
|
||||
implicit val testSet: Format[Set[TestDefinition]] = Format.set
|
||||
implicit val testSet: Format[Set[Discovered]] = Format.set
|
||||
override protected def backedMaps =
|
||||
Backed(testMap, TestsLabel, TestsFileName) ::
|
||||
Backed(applicationsMap, ApplicationsLabel, ApplicationsFileName) ::
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ trait AnalysisCallback extends NotNull
|
|||
{
|
||||
/** The names of classes that the analyzer should find subclasses of.*/
|
||||
def superclassNames: Iterable[String]
|
||||
/** The names of annotations that the analyzer should look for on classes and methods. */
|
||||
def annotationNames: Iterable[String]
|
||||
/** The base path for the project.*/
|
||||
def basePath: Path
|
||||
/** Called when the the given superclass could not be found on the classpath by the compiler.*/
|
||||
|
|
@ -36,6 +38,8 @@ trait AnalysisCallback extends NotNull
|
|||
/** Called when the a subclass of one of the classes given in <code>superclassNames</code> is
|
||||
* discovered.*/
|
||||
def foundSubclass(sourcePath: Path, subclassName: String, superclassName: String, isModule: Boolean): Unit
|
||||
/** Called when a class or one of its methods has an annotation listed in <code>annotationNames</code>*/
|
||||
def foundAnnotated(source: Path, className: String, annotationName: String, isModule: Boolean): Unit
|
||||
/** Called to indicate that the source file <code>sourcePath</code> depends on the source file
|
||||
* <code>dependsOnPath</code>.*/
|
||||
def sourceDependency(dependsOnPath: Path, sourcePath: Path): Unit
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ private sealed abstract class BasicBuilderProject extends InternalProject
|
|||
new BasicAnalysisCallback(info.projectPath, analysis)
|
||||
{
|
||||
def superclassNames = List(Project.ProjectClassName)
|
||||
def annotationNames = Nil
|
||||
def foundApplication(sourcePath: Path, className: String) {}
|
||||
def foundAnnotated(sourcePath: Path, subclassName: String, annotationName: String, isModule: Boolean) {}
|
||||
def foundSubclass(sourcePath: Path, subclassName: String, superclassName: String, isModule: Boolean)
|
||||
{
|
||||
if(superclassName == Project.ProjectClassName && !isModule)
|
||||
|
|
|
|||
|
|
@ -128,16 +128,22 @@ private final class AnalysisInterface(delegate: AnalysisCallback, basePath: Path
|
|||
{
|
||||
val outputPath = Path.fromFile(outputDirectory)
|
||||
def superclassNames = delegate.superclassNames.toSeq.toArray[String]
|
||||
def annotationNames = delegate.annotationNames.toSeq.toArray[String]
|
||||
def superclassNotFound(superclassName: String) = delegate.superclassNotFound(superclassName)
|
||||
def beginSource(source: File) = delegate.beginSource(srcPath(source))
|
||||
|
||||
def foundSubclass(source: File, subclassName: String, superclassName: String, isModule: Boolean) =
|
||||
delegate.foundSubclass(srcPath(source), subclassName, superclassName, isModule)
|
||||
def foundAnnotated(source: File, className: String, annotationName: String, isModule: Boolean) =
|
||||
delegate.foundAnnotated(srcPath(source), className, annotationName, isModule)
|
||||
def foundApplication(source: File, className: String) = delegate.foundApplication(srcPath(source), className)
|
||||
|
||||
def sourceDependency(dependsOn: File, source: File) =
|
||||
delegate.sourceDependency(srcPath(dependsOn), srcPath(source))
|
||||
def jarDependency(jar: File, source: File) = delegate.jarDependency(jar, srcPath(source))
|
||||
def generatedClass(source: File, clazz: File) = delegate.generatedClass(srcPath(source), classPath(clazz))
|
||||
def endSource(source: File) = delegate.endSource(srcPath(source))
|
||||
def foundApplication(source: File, className: String) = delegate.foundApplication(srcPath(source), className)
|
||||
|
||||
def classDependency(clazz: File, source: File) =
|
||||
{
|
||||
val sourcePath = srcPath(source)
|
||||
|
|
|
|||
|
|
@ -205,8 +205,10 @@ abstract class AbstractCompileConfiguration extends NotNull
|
|||
}
|
||||
abstract class CompileConfiguration extends AbstractCompileConfiguration
|
||||
{
|
||||
def testDefinitionClassNames: Iterable[String]
|
||||
def testFingerprints: TestFingerprints
|
||||
}
|
||||
final case class TestFingerprints(superclassNames: Iterable[String], annotationNames: Iterable[String]) extends NotNull
|
||||
|
||||
import java.io.File
|
||||
class CompileConditional(override val config: CompileConfiguration, compiler: AnalyzingCompiler) extends AbstractCompileConditional(config, compiler)
|
||||
{
|
||||
|
|
@ -217,9 +219,13 @@ class CompileConditional(override val config: CompileConfiguration, compiler: An
|
|||
protected def analysisCallback = new CompileAnalysisCallback
|
||||
protected class CompileAnalysisCallback extends BasicCompileAnalysisCallback(projectPath, analysis)
|
||||
{
|
||||
def superclassNames = testDefinitionClassNames
|
||||
private[this] val fingerprints = testFingerprints
|
||||
def superclassNames = fingerprints.superclassNames
|
||||
def annotationNames = fingerprints.annotationNames
|
||||
def foundSubclass(sourcePath: Path, subclassName: String, superclassName: String, isModule: Boolean): Unit =
|
||||
analysis.addTest(sourcePath, TestDefinition(isModule, subclassName, superclassName))
|
||||
analysis.addTest(sourcePath, DiscoveredSubclass(isModule, subclassName, superclassName))
|
||||
def foundAnnotated(sourcePath: Path, className: String, annotationName: String, isModule: Boolean): Unit =
|
||||
analysis.addTest(sourcePath, DiscoveredAnnotated(isModule, className, annotationName))
|
||||
}
|
||||
}
|
||||
abstract class AbstractCompileConditional(val config: AbstractCompileConfiguration, val compiler: AnalyzingCompiler) extends Conditional[Path, Path, File]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import ScalaProject.{optionsAsString, javaOptionsAsString}
|
|||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.Attributes
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
/** This class defines concrete instances of actions from ScalaProject using overridable paths,
|
||||
* options, and configuration. */
|
||||
|
|
@ -179,7 +180,7 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
|
|||
def testFrameworks: Seq[TestFramework] =
|
||||
{
|
||||
import TestFrameworks.{JUnit, ScalaCheck, ScalaTest, Specs, ScalaCheckCompat, ScalaTestCompat, SpecsCompat}
|
||||
ScalaCheck :: Specs :: ScalaTest :: JUnit :: ScalaCheckCompat :: ScalaTestCompat :: SpecsCompat :: Nil
|
||||
ScalaCheck :: Specs :: ScalaTest :: ScalaCheckCompat :: ScalaTestCompat :: SpecsCompat :: JUnit :: Nil
|
||||
}
|
||||
/** The list of listeners for testing. */
|
||||
def testListeners: Seq[TestReportListener] = TestLogger(log) :: Nil
|
||||
|
|
@ -197,12 +198,18 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
|
|||
def options = optionsAsString(baseCompileOptions.filter(!_.isInstanceOf[MaxCompileErrors]))
|
||||
def maxErrors = maximumErrors(baseCompileOptions)
|
||||
def compileOrder = BasicScalaProject.this.compileOrder
|
||||
protected def testClassNames(frameworks: Seq[TestFramework]) =
|
||||
protected def fingerprints(frameworks: Seq[TestFramework]): TestFingerprints =
|
||||
{
|
||||
import org.scalatools.testing.{SubclassFingerprint, AnnotatedFingerprint}
|
||||
val loader = TestFramework.createTestLoader(classpath.get, buildScalaInstance.loader)
|
||||
def getTestNames(framework: TestFramework): Seq[String] =
|
||||
framework.create(loader, log).toList.flatMap(_.tests.map(_.superClassName))
|
||||
frameworks.flatMap(getTestNames)
|
||||
val annotations = new ListBuffer[String]
|
||||
val superclasses = new ListBuffer[String]
|
||||
frameworks flatMap { _.create(loader, log) } flatMap(TestFramework.getTests) foreach {
|
||||
case s: SubclassFingerprint => superclasses += s.superClassName
|
||||
case a: AnnotatedFingerprint => annotations += a.annotationName
|
||||
case _ => ()
|
||||
}
|
||||
TestFingerprints(superclasses.toList, annotations.toList)
|
||||
}
|
||||
}
|
||||
class MainCompileConfig extends BaseCompileConfig
|
||||
|
|
@ -214,7 +221,7 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
|
|||
def outputDirectory = mainCompilePath
|
||||
def classpath = compileClasspath
|
||||
def analysisPath = mainAnalysisPath
|
||||
def testDefinitionClassNames: Seq[String] = Nil
|
||||
def testFingerprints = TestFingerprints(Nil, Nil)
|
||||
def javaOptions = javaOptionsAsString(javaCompileOptions)
|
||||
}
|
||||
class TestCompileConfig extends BaseCompileConfig
|
||||
|
|
@ -226,7 +233,7 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
|
|||
def outputDirectory = testCompilePath
|
||||
def classpath = testClasspath
|
||||
def analysisPath = testAnalysisPath
|
||||
def testDefinitionClassNames: Seq[String] = testClassNames(testFrameworks)
|
||||
def testFingerprints = fingerprints(testFrameworks)
|
||||
def javaOptions = javaOptionsAsString(testJavaCompileOptions)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ object Format
|
|||
def fromString(s: String) = (new HashSet[T]) ++ FileUtilities.pathSplit(s).map(_.trim).filter(!_.isEmpty).map(format.fromString)
|
||||
}
|
||||
implicit val string: Format[String] = new SimpleFormat[String] { def fromString(s: String) = s }
|
||||
implicit val test: Format[TestDefinition] = new SimpleFormat[TestDefinition]
|
||||
implicit val test: Format[Discovered] = new SimpleFormat[Discovered]
|
||||
{
|
||||
def fromString(s: String) = TestParser.parse(s).fold(error, x => x)
|
||||
def fromString(s: String) = DiscoveredParser.parse(s).fold(error, x => x)
|
||||
}
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ trait BasicIntegrationTesting extends ScalaIntegrationTesting with IntegrationTe
|
|||
def analysisPath = integrationTestAnalysisPath
|
||||
def baseCompileOptions = integrationTestCompileOptions
|
||||
def javaOptions = javaOptionsAsString(javaCompileOptions)
|
||||
def testDefinitionClassNames = testClassNames(integrationTestFrameworks)
|
||||
def testFingerprints = fingerprints(integrationTestFrameworks)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -302,16 +302,16 @@ trait ScalaProject extends SimpleScalaProject with FileTasks with MultiTaskProje
|
|||
log.debug("Excluding tests: ")
|
||||
excludeTestsSet.foreach(test => log.debug("\t" + test))
|
||||
}
|
||||
def includeTest(test: TestDefinition) = !excludeTestsSet.contains(test.testClassName) && testFilters.forall(filter => filter(test.testClassName))
|
||||
val tests = HashSet.empty[TestDefinition] ++ analysis.allTests.filter(includeTest)
|
||||
def includeTest(test: TestDefinition) = !excludeTestsSet.contains(test.name) && testFilters.forall(filter => filter(test.name))
|
||||
val tests = HashSet.empty[TestDefinition] ++ analysis.allTests.map(_.toDefinition).filter(includeTest)
|
||||
TestFramework.testTasks(frameworks, classpath.get, buildScalaInstance.loader, tests.toSeq, log,
|
||||
testListeners.readOnly, false, setup.readOnly, cleanup.readOnly, testArgsByFramework)
|
||||
}
|
||||
private def flatten[T](i: Iterable[Iterable[T]]) = i.flatMap(x => x)
|
||||
|
||||
protected def testQuickMethod(testAnalysis: CompileAnalysis, options: => Seq[TestOption])(toRun: (Seq[TestOption]) => Task) = {
|
||||
val analysis = testAnalysis.allTests.map(_.testClassName).toList
|
||||
multiTask(analysis) { (args, includeFunction) =>
|
||||
val analysis = Set() ++ testAnalysis.allTests.map(_.className)
|
||||
multiTask(analysis.toList) { (args, includeFunction) =>
|
||||
toRun(TestArgument(args:_*) :: TestFilter(includeFunction) :: options.toList)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
package sbt
|
||||
|
||||
import java.net.URLClassLoader
|
||||
import org.scalatools.testing.{TestFingerprint => Fingerprint, Framework,
|
||||
Runner, Logger=>TLogger, EventHandler, Event}
|
||||
import org.scalatools.testing.{AnnotatedFingerprint, Fingerprint, SubclassFingerprint, TestFingerprint}
|
||||
import org.scalatools.testing.{Event, EventHandler, Framework, Runner, Runner2, Logger=>TLogger}
|
||||
|
||||
object Result extends Enumeration
|
||||
{
|
||||
|
|
@ -32,36 +32,54 @@ class TestFramework(val implClassName: String) extends NotNull
|
|||
catch { case e: ClassNotFoundException => log.debug("Framework implementation '" + implClassName + "' not present."); None }
|
||||
}
|
||||
}
|
||||
final class TestDefinition(val name: String, val fingerprint: Fingerprint) extends NotNull
|
||||
{
|
||||
override def toString = "Test " + name + " : " + fingerprint
|
||||
override def equals(t: Any) =
|
||||
t match
|
||||
{
|
||||
case r: TestDefinition => name == r.name && TestFramework.matches(fingerprint, r.fingerprint)
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
final class TestRunner(framework: Framework, loader: ClassLoader, listeners: Seq[TestReportListener], log: Logger) extends NotNull
|
||||
{
|
||||
private[this] val delegate = framework.testRunner(loader, listeners.flatMap(_.contentLogger).toArray)
|
||||
private[this] def run(testDefinition: TestDefinition, handler: EventHandler, args: Array[String]): Unit =
|
||||
(testDefinition.fingerprint, delegate) match
|
||||
{
|
||||
case (simple: TestFingerprint, _) => delegate.run(testDefinition.name, simple, handler, args)
|
||||
case (basic, runner2: Runner2) => runner2.run(testDefinition.name, basic, handler, args)
|
||||
case _ => error("Framework '" + framework + "' does not support test '" + testDefinition + "'")
|
||||
}
|
||||
|
||||
final def run(testDefinition: TestDefinition, args: Seq[String]): Result.Value =
|
||||
{
|
||||
log.debug("Running " + testDefinition + " with arguments " + args.mkString(", "))
|
||||
val testClass = testDefinition.testClassName
|
||||
val name = testDefinition.name
|
||||
def runTest() =
|
||||
{
|
||||
// here we get the results! here is where we'd pass in the event listener
|
||||
val results = new scala.collection.mutable.ListBuffer[Event]
|
||||
val handler = new EventHandler { def handle(e:Event){ results += e } }
|
||||
delegate.run(testClass, testDefinition, handler, args.toArray)
|
||||
run(testDefinition, handler, args.toArray)
|
||||
val event = TestEvent(results)
|
||||
safeListenersCall(_.testEvent( event ))
|
||||
event.result
|
||||
}
|
||||
|
||||
safeListenersCall(_.startGroup(testClass))
|
||||
safeListenersCall(_.startGroup(name))
|
||||
try
|
||||
{
|
||||
val result = runTest().getOrElse(Result.Passed)
|
||||
safeListenersCall(_.endGroup(testClass, result))
|
||||
safeListenersCall(_.endGroup(name, result))
|
||||
result
|
||||
}
|
||||
catch
|
||||
{
|
||||
case e =>
|
||||
safeListenersCall(_.endGroup(testClass, e))
|
||||
safeListenersCall(_.endGroup(name, e))
|
||||
Result.Error
|
||||
}
|
||||
}
|
||||
|
|
@ -74,16 +92,14 @@ final class NamedTestTask(val name: String, action: => Option[String]) extends N
|
|||
|
||||
object TestFramework
|
||||
{
|
||||
// def runTests(frameworks: Seq[TestFramework], classpath: Iterable[Path], scalaLoader: ClassLoader,
|
||||
// tests: Seq[TestDefinition], testArgs: Seq[String], log: Logger, listeners: Seq[TestReportListener]) =
|
||||
// {
|
||||
// val (start, runTests, end) = testTasks(frameworks, classpath, scalaLoader, tests, log, listeners, true, Nil, Nil, Nil)
|
||||
// def run(tasks: Iterable[NamedTestTask]) = tasks.foreach(_.run())
|
||||
// run(start)
|
||||
// run(runTests)
|
||||
// run(end)
|
||||
// }
|
||||
|
||||
def getTests(framework: Framework): Seq[Fingerprint] =
|
||||
framework.getClass.getMethod("tests").invoke(framework) match
|
||||
{
|
||||
case newStyle: Array[Fingerprint] => newStyle.toList
|
||||
case oldStyle: Array[TestFingerprint] => oldStyle.toList
|
||||
case _ => error("Could not call 'tests' on framework " + framework)
|
||||
}
|
||||
|
||||
private val ScalaCompilerJarPackages = "scala.tools.nsc." :: "jline." :: "ch.epfl.lamp." :: Nil
|
||||
|
||||
private val TestStartName = "test-start"
|
||||
|
|
@ -92,6 +108,14 @@ object TestFramework
|
|||
private[sbt] def safeForeach[T](it: Iterable[T], log: Logger)(f: T => Unit): Unit =
|
||||
it.foreach(i => Control.trapAndLog(log){ f(i) } )
|
||||
|
||||
def matches(a: Fingerprint, b: Fingerprint) =
|
||||
(a, b) match
|
||||
{
|
||||
case (a: SubclassFingerprint, b: SubclassFingerprint) => a.isModule == b.isModule && a.superClassName == b.superClassName
|
||||
case (a: AnnotatedFingerprint, b: AnnotatedFingerprint) => a.isModule == b.isModule && a.annotationName == b.annotationName
|
||||
case _ => false
|
||||
}
|
||||
|
||||
import scala.collection.{immutable, Map, Set}
|
||||
|
||||
def testTasks(frameworks: Seq[TestFramework],
|
||||
|
|
@ -127,11 +151,7 @@ object TestFramework
|
|||
{
|
||||
for(test <- tests if !map.values.exists(_.contains(test)))
|
||||
{
|
||||
def isTestForFramework(framework: Framework) = framework.tests.exists(matches)
|
||||
def matches(fingerprint: Fingerprint) =
|
||||
(fingerprint.isModule == test.isModule) &&
|
||||
fingerprint.superClassName == test.superClassName
|
||||
|
||||
def isTestForFramework(framework: Framework) = getTests(framework).exists {t => matches(t, test.fingerprint) }
|
||||
for(framework <- frameworks.find(isTestForFramework))
|
||||
map.getOrElseUpdate(framework, new HashSet[TestDefinition]) += test
|
||||
}
|
||||
|
|
@ -180,7 +200,7 @@ object TestFramework
|
|||
Thread.currentThread.setContextClassLoader(oldLoader)
|
||||
}
|
||||
}
|
||||
new NamedTestTask(testDefinition.testClassName, runTest())
|
||||
new NamedTestTask(testDefinition.name, runTest())
|
||||
}
|
||||
}
|
||||
def end() =
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import sbt._
|
|||
import scala.collection.mutable
|
||||
import mutable.{ArrayBuffer, Buffer}
|
||||
import java.io.File
|
||||
import java.lang.annotation.Annotation
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier.{STATIC, PUBLIC, ABSTRACT}
|
||||
|
||||
private[sbt] object Analyze
|
||||
{
|
||||
|
|
@ -17,7 +20,11 @@ private[sbt] object Analyze
|
|||
val sourceSet = Set(sources.toSeq : _*)
|
||||
val classesFinder = outputDirectory ** GlobFilter("*.class")
|
||||
val existingClasses = classesFinder.get
|
||||
|
||||
|
||||
def load(tpe: String, errMsg: => String): Option[Class[_]] =
|
||||
try { Some(Class.forName(tpe, false, loader)) }
|
||||
catch { case e => log.warn(errMsg + " : " +e.toString); None }
|
||||
|
||||
// runs after compilation
|
||||
def analyze()
|
||||
{
|
||||
|
|
@ -26,7 +33,12 @@ private[sbt] object Analyze
|
|||
|
||||
val productToSource = new mutable.HashMap[Path, Path]
|
||||
val sourceToClassFiles = new mutable.HashMap[Path, Buffer[ClassFile]]
|
||||
|
||||
|
||||
val superclasses = analysis.superclassNames flatMap { tpe => load(tpe, "Could not load superclass '" + tpe + "'") }
|
||||
val annotations = analysis.annotationNames
|
||||
|
||||
def annotated(fromClass: Seq[Annotation]) = if(fromClass.isEmpty) Nil else annotations.filter(Set() ++ fromClass.map(_.annotationType.getName))
|
||||
|
||||
// parse class files and assign classes to sources. This must be done before dependencies, since the information comes
|
||||
// as class->class dependencies that must be mapped back to source->class dependencies using the source+class assignment
|
||||
for(newClass <- newClasses;
|
||||
|
|
@ -45,15 +57,27 @@ private[sbt] object Analyze
|
|||
for( (source, classFiles) <- sourceToClassFiles )
|
||||
{
|
||||
for(classFile <- classFiles if isTopLevel(classFile);
|
||||
method <- classFile.methods; if method.isMain)
|
||||
analysis.foundApplication(source, classFile.className)
|
||||
cls <- load(classFile.className, "Could not load '" + classFile.className + "' to check for superclasses.") )
|
||||
{
|
||||
for(superclass <- superclasses)
|
||||
if(superclass.isAssignableFrom(cls))
|
||||
analysis.foundSubclass(source, classFile.className, superclass.getName, false)
|
||||
|
||||
val annotations = new ArrayBuffer[String]
|
||||
annotations ++= annotated(cls.getAnnotations)
|
||||
for(method <- cls.getMethods)
|
||||
{
|
||||
annotations ++= annotated(method.getAnnotations)
|
||||
if(isMain(method))
|
||||
analysis.foundApplication(source, classFile.className)
|
||||
}
|
||||
annotations.foreach { ann => analysis.foundAnnotated(source, classFile.className, ann, false) }
|
||||
}
|
||||
def processDependency(tpe: String)
|
||||
{
|
||||
Control.trapAndLog(log)
|
||||
{
|
||||
val loaded =
|
||||
try { Some(Class.forName(tpe, false, loader)) }
|
||||
catch { case e => log.warn("Problem processing dependencies of source " + source + " : " +e.toString); None }
|
||||
val loaded = load(tpe, "Problem processing dependencies of source " + source)
|
||||
for(clazz <- loaded; file <- Control.convertException(FileUtilities.classLocationFile(clazz)).right)
|
||||
{
|
||||
if(file.isDirectory)
|
||||
|
|
@ -104,4 +128,16 @@ private[sbt] object Analyze
|
|||
candidates
|
||||
}
|
||||
private def isTopLevel(classFile: ClassFile) = classFile.className.indexOf('$') < 0
|
||||
private lazy val unit = classOf[Unit]
|
||||
private lazy val strArray = List(classOf[Array[String]])
|
||||
|
||||
private def isMain(method: Method): Boolean =
|
||||
method.getName == "main" &&
|
||||
isMain(method.getModifiers) &&
|
||||
method.getReturnType == unit &&
|
||||
method.getParameterTypes.toList == strArray
|
||||
private def isMain(modifiers: Int): Boolean = (modifiers & mainModifiers) == mainModifiers && (modifiers & notMainModifiers) == 0
|
||||
|
||||
private val mainModifiers = STATIC | PUBLIC
|
||||
private val notMainModifiers = ABSTRACT
|
||||
}
|
||||
|
|
@ -6,42 +6,68 @@ package sbt
|
|||
/* The following implements the simple syntax for storing test definitions.
|
||||
* The syntax is:
|
||||
*
|
||||
* definition := isModule? className separator className
|
||||
* definition := isModule? className typeSpecific
|
||||
* isModule := '<module>'
|
||||
* separator := '<<'
|
||||
* typeSpecific := annotated | subclass
|
||||
*
|
||||
* subclass := subclassSeparator className
|
||||
* subclassSeparator := '<<'
|
||||
*
|
||||
* annotated := annotationSeparator annotationName
|
||||
* annotationSeparator := '@'
|
||||
*/
|
||||
|
||||
import scala.util.parsing.combinator._
|
||||
import scala.util.parsing.combinator._
|
||||
import org.scalatools.testing.{Fingerprint, AnnotatedFingerprint, TestFingerprint}
|
||||
import DiscoveredParser._
|
||||
|
||||
import TestParser._
|
||||
/** Represents a test implemented by 'testClassName' of type 'superClassName'.*/
|
||||
final case class TestDefinition(isModule: Boolean, testClassName: String, superClassName: String) extends org.scalatools.testing.TestFingerprint with NotNull
|
||||
sealed abstract class Discovered extends Fingerprint with NotNull
|
||||
{
|
||||
/** Whether a test is a module or a class*/
|
||||
def isModule: Boolean
|
||||
def className: String
|
||||
// for TestFingerprint
|
||||
def testClassName = className
|
||||
def toDefinition: TestDefinition = new TestDefinition(className, this)
|
||||
}
|
||||
/** Represents a class 'className' that has 'superClassName' as an ancestor.*/
|
||||
final case class DiscoveredSubclass(isModule: Boolean, className: String, superClassName: String) extends Discovered with TestFingerprint
|
||||
{
|
||||
override def toString =
|
||||
(if(isModule) IsModuleLiteral else "") + testClassName + SubSuperSeparator + superClassName
|
||||
(if(isModule) IsModuleLiteral else "") + className + SubSuperSeparator + superClassName
|
||||
}
|
||||
final class TestParser extends RegexParsers with NotNull
|
||||
/** Represents an annotation on a method or class.*/
|
||||
final case class DiscoveredAnnotated(isModule: Boolean, className: String, annotationName: String) extends Discovered with AnnotatedFingerprint
|
||||
{
|
||||
def test: Parser[TestDefinition] =
|
||||
( isModule ~! className ~! SubSuperSeparator ~! className ) ^^
|
||||
{ case module ~ testName ~ SubSuperSeparator ~ superName => TestDefinition(module, testName.trim, superName.trim) }
|
||||
override def toString =
|
||||
(if(isModule) IsModuleLiteral else "") + className + AnnotationSeparator + annotationName
|
||||
}
|
||||
final class DiscoveredParser extends RegexParsers with NotNull
|
||||
{
|
||||
def definition: Parser[Discovered] =
|
||||
( isModule ~! className ~! (SubSuperSeparator | AnnotationSeparator) ~! className ) ^^ {
|
||||
case module ~ name ~ SubSuperSeparator ~ superName => DiscoveredSubclass(module, name.trim, superName.trim)
|
||||
case module ~ name ~ AnnotationSeparator ~ annotation => DiscoveredAnnotated(module, name.trim, annotation.trim)
|
||||
}
|
||||
|
||||
def isModule: Parser[Boolean] = (IsModuleLiteral?) ^^ (_.isDefined)
|
||||
def className: Parser[String] = ClassNameRegexString.r
|
||||
|
||||
def parse(testDefinitionString: String): Either[String, TestDefinition] =
|
||||
def parse(definitionString: String): Either[String, Discovered] =
|
||||
{
|
||||
def parseError(msg: String) = Left("Could not parse test definition '" + testDefinitionString + "': " + msg)
|
||||
parseAll(test, testDefinitionString) match
|
||||
def parseError(msg: String) = Left("Could not parse discovered definition '" + definitionString + "': " + msg)
|
||||
parseAll(definition, definitionString) match
|
||||
{
|
||||
case Success(result, next) => Right(result)
|
||||
case err: NoSuccess => parseError(err.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
object TestParser
|
||||
object DiscoveredParser
|
||||
{
|
||||
val IsModuleLiteral = "<module>"
|
||||
val SubSuperSeparator = "<<"
|
||||
val ClassNameRegexString = """[^<]+"""
|
||||
def parse(testDefinitionString: String): Either[String, TestDefinition] = (new TestParser).parse(testDefinitionString)
|
||||
val AnnotationSeparator = "@"
|
||||
val ClassNameRegexString = """[^@<]+"""
|
||||
def parse(definitionString: String): Either[String, Discovered] = (new DiscoveredParser).parse(definitionString)
|
||||
}
|
||||
Loading…
Reference in New Issue