diff --git a/compile/interface/Analyzer.scala b/compile/interface/Analyzer.scala
index 2c08adad5..4b3e99ae4 100644
--- a/compile/interface/Analyzer.scala
+++ b/compile/interface/Analyzer.scala
@@ -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)
diff --git a/interface/src/main/java/xsbti/AnalysisCallback.java b/interface/src/main/java/xsbti/AnalysisCallback.java
index 2372a408b..4db5f28c3 100644
--- a/interface/src/main/java/xsbti/AnalysisCallback.java
+++ b/interface/src/main/java/xsbti/AnalysisCallback.java
@@ -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 superclassNames is
* discovered.*/
public void foundSubclass(File source, String subclassName, String superclassName, boolean isModule);
+ /** Called when an annotation with name annotationName 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 source depends on the source file
* dependsOn. 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
diff --git a/notes b/notes
index 5c46554d8..1d709445b 100644
--- a/notes
+++ b/notes
@@ -1,19 +1,34 @@
DONE
- - fixed Jetty 7 and JRebel
- - fixed make-pom and missing 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-.jar if available or a copy of the current launcher jar that uses 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)
\ No newline at end of file
+- see launch.specification
\ No newline at end of file
diff --git a/project/build/SbtProject.scala b/project/build/SbtProject.scala
index 823aee142..985d02412 100644
--- a/project/build/SbtProject.scala
+++ b/project/build/SbtProject.scala
@@ -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)
diff --git a/sbt/src/main/scala/sbt/Analysis.scala b/sbt/src/main/scala/sbt/Analysis.scala
index 1de5a403b..da117c33f 100644
--- a/sbt/src/main/scala/sbt/Analysis.scala
+++ b/sbt/src/main/scala/sbt/Analysis.scala
@@ -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) ::
diff --git a/sbt/src/main/scala/sbt/AnalysisCallback.scala b/sbt/src/main/scala/sbt/AnalysisCallback.scala
index 6ac5d606b..1fb7e4d31 100644
--- a/sbt/src/main/scala/sbt/AnalysisCallback.scala
+++ b/sbt/src/main/scala/sbt/AnalysisCallback.scala
@@ -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 superclassNames 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 annotationNames*/
+ def foundAnnotated(source: Path, className: String, annotationName: String, isModule: Boolean): Unit
/** Called to indicate that the source file sourcePath depends on the source file
* dependsOnPath.*/
def sourceDependency(dependsOnPath: Path, sourcePath: Path): Unit
diff --git a/sbt/src/main/scala/sbt/BuilderProject.scala b/sbt/src/main/scala/sbt/BuilderProject.scala
index 793c7c1b9..e03a982ee 100644
--- a/sbt/src/main/scala/sbt/BuilderProject.scala
+++ b/sbt/src/main/scala/sbt/BuilderProject.scala
@@ -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)
diff --git a/sbt/src/main/scala/sbt/Compile.scala b/sbt/src/main/scala/sbt/Compile.scala
index 50e4fc7de..917387bcb 100644
--- a/sbt/src/main/scala/sbt/Compile.scala
+++ b/sbt/src/main/scala/sbt/Compile.scala
@@ -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)
diff --git a/sbt/src/main/scala/sbt/Conditional.scala b/sbt/src/main/scala/sbt/Conditional.scala
index d02c8e753..453c23f31 100644
--- a/sbt/src/main/scala/sbt/Conditional.scala
+++ b/sbt/src/main/scala/sbt/Conditional.scala
@@ -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]
diff --git a/sbt/src/main/scala/sbt/DefaultProject.scala b/sbt/src/main/scala/sbt/DefaultProject.scala
index 8397d254b..c8ff2105e 100644
--- a/sbt/src/main/scala/sbt/DefaultProject.scala
+++ b/sbt/src/main/scala/sbt/DefaultProject.scala
@@ -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)
}
diff --git a/sbt/src/main/scala/sbt/Format.scala b/sbt/src/main/scala/sbt/Format.scala
index fa7cceb5d..5796b0686 100644
--- a/sbt/src/main/scala/sbt/Format.scala
+++ b/sbt/src/main/scala/sbt/Format.scala
@@ -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)
}
}
\ No newline at end of file
diff --git a/sbt/src/main/scala/sbt/IntegrationTesting.scala b/sbt/src/main/scala/sbt/IntegrationTesting.scala
index 1957f0b4a..158f7d040 100644
--- a/sbt/src/main/scala/sbt/IntegrationTesting.scala
+++ b/sbt/src/main/scala/sbt/IntegrationTesting.scala
@@ -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)
}
}
diff --git a/sbt/src/main/scala/sbt/ScalaProject.scala b/sbt/src/main/scala/sbt/ScalaProject.scala
index 4e3d1a9ba..f9a83274d 100644
--- a/sbt/src/main/scala/sbt/ScalaProject.scala
+++ b/sbt/src/main/scala/sbt/ScalaProject.scala
@@ -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)
}
}
diff --git a/sbt/src/main/scala/sbt/TestFramework.scala b/sbt/src/main/scala/sbt/TestFramework.scala
index 14e432979..f06223490 100644
--- a/sbt/src/main/scala/sbt/TestFramework.scala
+++ b/sbt/src/main/scala/sbt/TestFramework.scala
@@ -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() =
diff --git a/sbt/src/main/scala/sbt/classfile/Analyze.scala b/sbt/src/main/scala/sbt/classfile/Analyze.scala
index 1f54bdf81..a4debb136 100644
--- a/sbt/src/main/scala/sbt/classfile/Analyze.scala
+++ b/sbt/src/main/scala/sbt/classfile/Analyze.scala
@@ -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
}
\ No newline at end of file
diff --git a/sbt/src/main/scala/sbt/impl/TestParser.scala b/sbt/src/main/scala/sbt/impl/TestParser.scala
index d12bc9319..be4b3949c 100644
--- a/sbt/src/main/scala/sbt/impl/TestParser.scala
+++ b/sbt/src/main/scala/sbt/impl/TestParser.scala
@@ -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 := ''
-* 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 = ""
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)
}
\ No newline at end of file