diff --git a/project/build/SbtProject.scala b/project/build/SbtProject.scala index 4bf40940b..4132e5e74 100644 --- a/project/build/SbtProject.scala +++ b/project/build/SbtProject.scala @@ -52,6 +52,8 @@ class SbtProject(info: ProjectInfo) extends DefaultProject(info) //with test.Sbt val jsch = "com.jcraft" % "jsch" % "0.1.31" intransitive() val jetty = "org.mortbay.jetty" % "jetty" % "6.1.14" % "optional" + val testInterface = "org.scala-tools.testing" % "test-interface" % "0.1" + // xsbt components val xsbti = "org.scala-tools.sbt" % "launcher-interface" % projectVersion.value.toString % "provided" val compiler = "org.scala-tools.sbt" %% "compile" % projectVersion.value.toString diff --git a/src/main/scala/sbt/DefaultProject.scala b/src/main/scala/sbt/DefaultProject.scala index d14acc354..2ee7efedb 100644 --- a/src/main/scala/sbt/DefaultProject.scala +++ b/src/main/scala/sbt/DefaultProject.scala @@ -12,6 +12,7 @@ class DefaultWebProject(val info: ProjectInfo) extends BasicWebScalaProject with import BasicScalaProject._ import ScalaProject.{optionsAsString, javaOptionsAsString} import java.io.File +import java.net.URLClassLoader import java.util.jar.Attributes /** This class defines concrete instances of actions from ScalaProject using overridable paths, @@ -169,9 +170,13 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec /** The list of test frameworks to use for testing. Note that adding frameworks to this list * for an active project currently requires an explicit 'clean' to properly update the set of tests to * run*/ - def testFrameworks: Iterable[TestFramework] = ScalaCheckFramework :: SpecsFramework :: ScalaTestFramework :: Nil + def testFrameworks: Seq[TestFramework] = + { + import TestFrameworks.{ScalaCheck, ScalaTest, Specs} + /*ScalaCheckFramework :: SpecsFramework ::*/ ScalaTest :: Nil + } /** The list of listeners for testing. */ - def testListeners: Seq[TestReportListener] = new LogTestReportListener(log) :: Nil + def testListeners: Seq[TestReportListener] = TestLogger(log) :: Nil def mainLabel = "main" def testLabel = "test" @@ -186,6 +191,14 @@ 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]) = + { + val loader = new URLClassLoader(classpath.get.map(_.asURL).toSeq.toArray) + def getTestNames(framework: TestFramework): Seq[String] = + try { framework.create(loader).tests.map(_.superClassName) } + catch { case e: ClassNotFoundException => log.debug("Framework implementation '" + framework.implClassName + "' not present."); Nil } + frameworks.flatMap(getTestNames) + } } class MainCompileConfig extends BaseCompileConfig { @@ -196,7 +209,7 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec def outputDirectory = mainCompilePath def classpath = compileClasspath def analysisPath = mainAnalysisPath - def testDefinitionClassNames: Iterable[String] = Nil + def testDefinitionClassNames: Seq[String] = Nil def javaOptions = javaOptionsAsString(javaCompileOptions) } class TestCompileConfig extends BaseCompileConfig @@ -208,7 +221,7 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec def outputDirectory = testCompilePath def classpath = testClasspath def analysisPath = testAnalysisPath - def testDefinitionClassNames: Iterable[String] = testFrameworks.map(_.testSuperClassName) + def testDefinitionClassNames: Seq[String] = testClassNames(testFrameworks) def javaOptions = javaOptionsAsString(testJavaCompileOptions) } diff --git a/src/main/scala/sbt/IntegrationTesting.scala b/src/main/scala/sbt/IntegrationTesting.scala index 2b7f3a283..f31797747 100644 --- a/src/main/scala/sbt/IntegrationTesting.scala +++ b/src/main/scala/sbt/IntegrationTesting.scala @@ -15,7 +15,7 @@ trait IntegrationTesting extends NotNull trait ScalaIntegrationTesting extends IntegrationTesting { self: ScalaProject => - protected def integrationTestTask(frameworks: Iterable[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]) = + protected def integrationTestTask(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]) = testTask(frameworks, classpath, analysis, options) } @@ -83,7 +83,7 @@ trait BasicIntegrationTesting extends ScalaIntegrationTesting with IntegrationTe def analysisPath = integrationTestAnalysisPath def baseCompileOptions = integrationTestCompileOptions def javaOptions = javaOptionsAsString(javaCompileOptions) - def testDefinitionClassNames = integrationTestFrameworks.map(_.testSuperClassName) + def testDefinitionClassNames = testClassNames(integrationTestFrameworks) } } diff --git a/src/main/scala/sbt/Logger.scala b/src/main/scala/sbt/Logger.scala index 78e2ad979..b4d9de0d3 100644 --- a/src/main/scala/sbt/Logger.scala +++ b/src/main/scala/sbt/Logger.scala @@ -24,6 +24,7 @@ abstract class Logger extends xsbt.CompileLogger with xsbt.IvyLogger def setLevel(newLevel: Level.Value) def enableTrace(flag: Boolean) def traceEnabled: Boolean + def ansiCodesSupported = false def atLevel(level: Level.Value) = level.id >= getLevel.id def trace(t: => Throwable): Unit @@ -73,6 +74,7 @@ abstract class BasicLogger extends Logger final class SynchronizedLogger(delegate: Logger) extends Logger { + override lazy val ansiCodesSupported = delegate.ansiCodesSupported def getLevel = { synchronized { delegate.getLevel } } def setLevel(newLevel: Level.Value) { synchronized { delegate.setLevel(newLevel) } } def enableTrace(enabled: Boolean) { synchronized { delegate.enableTrace(enabled) } } @@ -87,6 +89,7 @@ final class SynchronizedLogger(delegate: Logger) extends Logger final class MultiLogger(delegates: List[Logger]) extends BasicLogger { + override lazy val ansiCodesSupported = delegates.forall(_.ansiCodesSupported) override def setLevel(newLevel: Level.Value) { super.setLevel(newLevel) @@ -110,6 +113,7 @@ final class MultiLogger(delegates: List[Logger]) extends BasicLogger * */ final class FilterLogger(delegate: Logger) extends BasicLogger { + override lazy val ansiCodesSupported = delegate.ansiCodesSupported def trace(t: => Throwable) { if(traceEnabled) @@ -146,6 +150,7 @@ final class FilterLogger(delegate: Logger) extends BasicLogger * */ final class BufferedLogger(delegate: Logger) extends Logger { + override lazy val ansiCodesSupported = delegate.ansiCodesSupported private[this] val buffers = wrap.Wrappers.weakMap[Thread, Buffer[LogEvent]] private[this] var recordingAll = false @@ -271,7 +276,7 @@ object ConsoleLogger * This logger is not thread-safe.*/ class ConsoleLogger extends BasicLogger { - import ConsoleLogger.formatEnabled + override def ansiCodesSupported = ConsoleLogger.formatEnabled def messageColor(level: Level.Value) = Console.RESET def labelColor(level: Level.Value) = level match @@ -300,7 +305,7 @@ class ConsoleLogger extends BasicLogger } private def setColor(color: String) { - if(formatEnabled) + if(ansiCodesSupported) System.out.synchronized { System.out.print(color) } } private def log(labelColor: String, label: String, messageColor: String, message: String): Unit = diff --git a/src/main/scala/sbt/ScalaProject.scala b/src/main/scala/sbt/ScalaProject.scala index d03847568..dee6d3215 100644 --- a/src/main/scala/sbt/ScalaProject.scala +++ b/src/main/scala/sbt/ScalaProject.scala @@ -149,9 +149,9 @@ trait ScalaProject extends SimpleScalaProject with FileTasks with MultiTaskProje def copyTask(sources: PathFinder, destinationDirectory: Path): Task = task { FileUtilities.copy(sources.get, destinationDirectory, log).left.toOption } - def testTask(frameworks: Iterable[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: TestOption*): Task = + def testTask(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: TestOption*): Task = testTask(frameworks, classpath, analysis, options) - def testTask(frameworks: Iterable[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]): Task = + def testTask(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]): Task = { def work = { @@ -249,7 +249,7 @@ trait ScalaProject extends SimpleScalaProject with FileTasks with MultiTaskProje } } protected def incrementImpl(v: BasicVersion): Version = v.incrementMicro - protected def testTasks(frameworks: Iterable[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]) = { + protected def testTasks(frameworks: Seq[TestFramework], classpath: PathFinder, analysis: CompileAnalysis, options: => Seq[TestOption]) = { import scala.collection.mutable.HashSet val testFilters = new ListBuffer[String => Boolean] @@ -267,7 +267,6 @@ trait ScalaProject extends SimpleScalaProject with FileTasks with MultiTaskProje case TestSetup(setupFunction) => setup += setupFunction case TestCleanup(cleanupFunction) => cleanup += cleanupFunction } - () // 2.8.0-SNAPSHOT bug in type inference } if(excludeTestsSet.size > 0 && log.atLevel(Level.Debug)) @@ -277,7 +276,7 @@ trait ScalaProject extends SimpleScalaProject with FileTasks with MultiTaskProje } def includeTest(test: TestDefinition) = !excludeTestsSet.contains(test.testClassName) && testFilters.forall(filter => filter(test.testClassName)) val tests = HashSet.empty[TestDefinition] ++ analysis.allTests.filter(includeTest) - TestFramework.testTasks(frameworks, classpath.get, tests, log, testListeners.readOnly, false, setup.readOnly, cleanup.readOnly) + TestFramework.testTasks(frameworks, classpath.get, tests.toSeq, log, testListeners.readOnly, false, setup.readOnly, cleanup.readOnly) } private def flatten[T](i: Iterable[Iterable[T]]) = i.flatMap(x => x) diff --git a/src/main/scala/sbt/TestFramework.scala b/src/main/scala/sbt/TestFramework.scala index e5bbf1bb3..d1ba04985 100644 --- a/src/main/scala/sbt/TestFramework.scala +++ b/src/main/scala/sbt/TestFramework.scala @@ -3,62 +3,63 @@ */ package sbt + import java.net.URLClassLoader + import org.scalatools.testing.{TestFingerprint => Fingerprint, Framework, Runner, Logger=>TLogger} + object Result extends Enumeration { val Error, Passed, Failed = Value } -object ClassType extends Enumeration -{ - val Module, Class = Value -} -trait TestFramework extends NotNull +object TestFrameworks { - def name: String - def testSuperClassName: String - def testSubClassType: ClassType.Value - - def testRunner(classLoader: ClassLoader, listeners: Iterable[TestReportListener], log: Logger): TestRunner + val ScalaCheck = new TestFramework("org.scalacheck.FrameworkImpl") + val ScalaTest = new TestFramework("org.scalatest.FrameworkImpl") + val Specs = new TestFramework("org.specs.FrameworkImpl") } -trait TestRunner extends NotNull +class TestFramework(val implClassName: String) extends NotNull { - def run(testClassName: String): Result.Value + def create(loader: ClassLoader): Framework = + Class.forName(implClassName, true, loader).newInstance.asInstanceOf[Framework] } - -abstract class BasicTestRunner extends TestRunner +final class TestRunner(framework: Framework, loader: ClassLoader, listeners: Seq[TestReportListener], log: Logger) extends NotNull { - protected def log: Logger - protected def listeners: Seq[TestReportListener] - - final def run(testClass: String): Result.Value = + private[this] val delegate = framework.testRunner(loader, listeners.flatMap(_.contentLogger).toArray) + final def run(testDefinition: TestDefinition, args: Seq[String]): Result.Value = { + val testClass = testDefinition.testClassName + def runTest() = + { + val results = delegate.run(testClass, testDefinition, args.toArray) + val event = TestEvent(results) + safeListenersCall(_.testEvent( event )) + event.result + } + safeListenersCall(_.startGroup(testClass)) try { - val result = runTest(testClass) + val result = runTest().getOrElse(Result.Passed) safeListenersCall(_.endGroup(testClass, result)) result } catch { case e => - { safeListenersCall(_.endGroup(testClass, e)) Result.Error - } } } - def runTest(testClass: String): Result.Value - protected def fire(event: TestEvent) = safeListenersCall(_.testEvent(event)) - protected def safeListenersCall(call: (TestReportListener) => Unit) = TestFramework.safeForeach(listeners, log)(call) + protected def safeListenersCall(call: (TestReportListener) => Unit): Unit = + TestFramework.safeForeach(listeners, log)(call) } final class NamedTestTask(val name: String, action: => Option[String]) extends NotNull { def run() = action } object TestFramework { - def runTests(frameworks: Iterable[TestFramework], classpath: Iterable[Path], tests: Iterable[TestDefinition], log: Logger, - listeners: Iterable[TestReportListener]) = + def runTests(frameworks: Seq[TestFramework], classpath: Iterable[Path], tests: Seq[TestDefinition], log: Logger, + listeners: Seq[TestReportListener]) = { val (start, runTests, end) = testTasks(frameworks, classpath, tests, log, listeners, true, Nil, Nil) def run(tasks: Iterable[NamedTestTask]) = tasks.foreach(_.run()) @@ -72,49 +73,55 @@ object TestFramework private val TestStartName = "test-start" private val TestFinishName = "test-finish" - private[sbt] def safeForeach[T](it: Iterable[T], log: Logger)(f: T => Unit): Unit = it.foreach(i => Control.trapAndLog(log){ f(i) } ) - import scala.collection.{Map, Set} - def testTasks(frameworks: Iterable[TestFramework], classpath: Iterable[Path], tests: Iterable[TestDefinition], log: Logger, - listeners: Iterable[TestReportListener], endErrorsEnabled: Boolean, setup: Iterable[() => Option[String]], + private[sbt] def safeForeach[T](it: Iterable[T], log: Logger)(f: T => Unit): Unit = + it.foreach(i => Control.trapAndLog(log){ f(i) } ) + + import scala.collection.{Map, Set} + + def testTasks(frameworks: Seq[TestFramework], classpath: Iterable[Path], tests: Seq[TestDefinition], log: Logger, + listeners: Seq[TestReportListener], endErrorsEnabled: Boolean, setup: Iterable[() => Option[String]], cleanup: Iterable[() => Option[String]]): (Iterable[NamedTestTask], Iterable[NamedTestTask], Iterable[NamedTestTask]) = { - val mappedTests = testMap(frameworks, tests) + val loader = createTestLoader(classpath) + val rawFrameworks = frameworks.map(_.create(loader)) + val mappedTests = testMap(rawFrameworks, tests) if(mappedTests.isEmpty) (new NamedTestTask(TestStartName, None) :: Nil, Nil, new NamedTestTask(TestFinishName, { log.info("No tests to run."); None }) :: Nil ) else - createTestTasks(classpath, mappedTests, log, listeners, endErrorsEnabled, setup, cleanup) + createTestTasks(loader, mappedTests, log, listeners, endErrorsEnabled, setup, cleanup) } - private def testMap(frameworks: Iterable[TestFramework], tests: Iterable[TestDefinition]): Map[TestFramework, Set[String]] = + private def testMap(frameworks: Seq[Framework], tests: Seq[TestDefinition]): Map[Framework, Set[TestDefinition]] = { import scala.collection.mutable.{HashMap, HashSet, Set} - val map = new HashMap[TestFramework, Set[String]] - if(!frameworks.isEmpty) + val map = new HashMap[Framework, Set[TestDefinition]] + def assignTests(): Unit = { - for(test <- tests) + for(test <- tests if !map.values.exists(_.contains(test))) { - def isTestForFramework(framework: TestFramework) = - (framework.testSubClassType == ClassType.Module) == test.isModule && - framework.testSuperClassName == test.superClassName + def isTestForFramework(framework: Framework) = framework.tests.exists(matches) + def matches(fingerprint: Fingerprint) = + (fingerprint.isModule == test.isModule) && + fingerprint.superClassName == test.superClassName for(framework <- frameworks.find(isTestForFramework)) - map.getOrElseUpdate(framework, new HashSet[String]) += test.testClassName + map.getOrElseUpdate(framework, new HashSet[TestDefinition]) += test } } + if(!frameworks.isEmpty) + assignTests() wrap.Wrappers.readOnly(map) } private def createTasks(work: Iterable[() => Option[String]], baseName: String) = work.toList.zipWithIndex.map{ case (work, index) => new NamedTestTask(baseName + " " + (index+1), work()) } - private def createTestTasks(classpath: Iterable[Path], tests: Map[TestFramework, Set[String]], log: Logger, - listeners: Iterable[TestReportListener], endErrorsEnabled: Boolean, setup: Iterable[() => Option[String]], + private def createTestTasks(loader: ClassLoader, tests: Map[Framework, Set[TestDefinition]], log: Logger, + listeners: Seq[TestReportListener], endErrorsEnabled: Boolean, setup: Iterable[() => Option[String]], cleanup: Iterable[() => Option[String]]) = { - val filterCompilerLoader = new FilteredLoader(getClass.getClassLoader, ScalaCompilerJarPackages) - val loader: ClassLoader = new IntermediateLoader(classpath.map(_.asURL).toSeq.toArray, filterCompilerLoader) val testsListeners = listeners.filter(_.isInstanceOf[TestsListener]).map(_.asInstanceOf[TestsListener]) def foreachListenerSafe(f: TestsListener => Unit): Unit = safeForeach(testsListeners, log)(f) - import Result.{Error,Passed,Failed} + import Result.{Error,Passed,Failed} object result { private[this] var value: Result.Value = Passed @@ -123,17 +130,17 @@ object TestFramework } val startTask = new NamedTestTask(TestStartName, {foreachListenerSafe(_.doInit); None}) :: createTasks(setup, "Test setup") val testTasks = - tests flatMap { case (framework, testClassNames) => + tests flatMap { case (framework, testDefinitions) => - val runner = framework.testRunner(loader, listeners, log) - for(testClassName <- testClassNames) yield + val runner = new TestRunner(framework, loader, listeners, log) + for(testDefinition <- testDefinitions) yield { def runTest() = { val oldLoader = Thread.currentThread.getContextClassLoader Thread.currentThread.setContextClassLoader(loader) try { - runner.run(testClassName) match + runner.run(testDefinition, Nil) match { case Error => result() = Error; Some("ERROR occurred during testing.") case Failed => result() = Failed; Some("Test FAILED") @@ -144,7 +151,7 @@ object TestFramework Thread.currentThread.setContextClassLoader(oldLoader) } } - new NamedTestTask(testClassName, runTest()) + new NamedTestTask(testDefinition.testClassName, runTest()) } } def end() = @@ -165,67 +172,9 @@ object TestFramework val endTask = new NamedTestTask(TestFinishName, end() ) :: createTasks(cleanup, "Test cleanup") (startTask, testTasks, endTask) } -} - -abstract class LazyTestFramework extends TestFramework -{ - /** The class name of the the test runner that executes - * tests for this framework.*/ - protected def testRunnerClassName: String - - protected def bundledTestRunnerClassName: Option[String] = None - - /** Creates an instance of the runner given by 'testRunnerClassName'.*/ - final def testRunner(projectLoader: ClassLoader, listeners: Iterable[TestReportListener], log: Logger): TestRunner = + private def createTestLoader(classpath: Iterable[Path]): ClassLoader = { - val frameworkClasspath = FileUtilities.classLocation(getClass) - val sbtURL = FileUtilities.sbtJar.toURI.toURL - val projectClasspath = projectLoader.asInstanceOf[java.net.URLClassLoader].getURLs - val loaderURLs = Array(frameworkClasspath, sbtURL) ++ projectClasspath - - def load(name: String): Option[Class[_]] = - { - val lazyLoader = new LazyFrameworkLoader(name, loaderURLs, projectLoader, getClass.getClassLoader) - try { Some(Class.forName(name, true, lazyLoader)) } - catch { case e: ClassNotFoundException => None } - } - val c = bundledTestRunnerClassName.flatMap(load).orElse(load(testRunnerClassName)) - val runnerClass = c.getOrElse(error("Could not find test runner")).asSubclass(classOf[TestRunner]) - runnerClass.getConstructor(classOf[Logger], classOf[Seq[TestReportListener]], classOf[ClassLoader]).newInstance(log, listeners, projectLoader) + val filterCompilerLoader = new FilteredLoader(getClass.getClassLoader, ScalaCompilerJarPackages) + new URLClassLoader(classpath.map(_.asURL).toSeq.toArray, filterCompilerLoader) } } - -/** The test framework definition for ScalaTest.*/ -object ScalaTestFramework extends LazyTestFramework -{ - val name = "ScalaTest" - val SuiteClassName = "org.scalatest.Suite" - - def testSuperClassName = SuiteClassName - def testSubClassType = ClassType.Class - - def testRunnerClassName = "sbt.impl.ScalaTestRunner" - override def bundledTestRunnerClassName = Some("org.scalatest.sbt.ScalaTestRunner") -} -/** The test framework definition for ScalaCheck.*/ -object ScalaCheckFramework extends LazyTestFramework -{ - val name = "ScalaCheck" - val PropertiesClassName = "org.scalacheck.Properties" - - def testSuperClassName = PropertiesClassName - def testSubClassType = ClassType.Module - - def testRunnerClassName = "sbt.impl.ScalaCheckRunner" -} -/** The test framework definition for specs.*/ -object SpecsFramework extends LazyTestFramework -{ - val name = "specs" - val SpecificationClassName = "org.specs.Specification" - - def testSuperClassName = SpecificationClassName - def testSubClassType = ClassType.Module - - def testRunnerClassName = "sbt.impl.SpecsRunner" -} \ No newline at end of file diff --git a/src/main/scala/sbt/TestReportListener.scala b/src/main/scala/sbt/TestReportListener.scala index e60be1ddc..701f83563 100644 --- a/src/main/scala/sbt/TestReportListener.scala +++ b/src/main/scala/sbt/TestReportListener.scala @@ -4,6 +4,8 @@ package sbt + import org.scalatools.testing.{Logger => TLogger, Event => TEvent, Result => TResult} + trait TestReportListener { /** called for each class or equivalent grouping */ @@ -14,6 +16,8 @@ trait TestReportListener def endGroup(name: String, t: Throwable) /** called if test completed */ def endGroup(name: String, result: Result.Value) + /** Used by the test framework for logging test results*/ + def contentLogger: Option[TLogger] = None } trait TestsListener extends TestReportListener @@ -24,317 +28,82 @@ trait TestsListener extends TestReportListener def doComplete(finalResult: Result.Value) } -abstract class WriterReportListener(val log: Logger) extends TestsListener -{ - import java.io.{IOException, PrintWriter, Writer} - import scala.collection.mutable.{Buffer, ListBuffer} - - protected case class Summary(count: Int, failures: Int, errors: Int, skipped: Int, message: Option[String], cause: Option[Throwable]) extends NotNull - private var out: Option[PrintWriter] = None - private var groupCount: Int = 0 - private var groupFailures: Int = 0 - private var groupErrors: Int = 0 - private var groupSkipped: Int = 0 - private var groupMessages: Seq[String] = Nil - - protected val passedEventHandler: TestEvent => Summary = (event: TestEvent) => event match - { - case SpecificationReportEvent(successes, failures, errors, skipped, desc, systems, subSpecs) => Summary(successes, failures, errors, skipped, None, None) - case IgnoredEvent(name, Some(message)) => Summary(1, 0, 0, 1, Some(message), None) - case IgnoredEvent(name, None) => Summary(1, 0, 0, 1, None, None) - case _ => Summary(1, 0, 0, 0, None, None) - } - protected val failedEventHandler: TestEvent => Summary = (event: TestEvent) => event match - { - case FailedEvent(name, msg) => Summary(1, 1, 0, 0, Some("! " + name + ": " + msg), None) - case TypedErrorEvent(name, event, Some(msg), cause) => Summary(1, 1, 0, 0, Some(event + " - " + name + ": " + msg), cause) - case TypedErrorEvent(name, event, None, cause) => Summary(1, 1, 0, 0, Some(event + " - " + name), cause) - case ErrorEvent(msg) => Summary(1, 1, 0, 0, Some(msg), None) - case SpecificationReportEvent(successes, failures, errors, skipped, desc, systems, subSpecs) => Summary(successes + failures + errors + skipped, failures, errors, skipped, Some(desc), None) - case _ => {log.warn("Unrecognized failure: " + event); Summary(1, 1, 0, 0, None, None)} - } - protected val errorEventHandler: TestEvent => Summary = (event: TestEvent) => event match - { - case FailedEvent(name, msg) => Summary(1, 0, 1, 0, Some("! " + name + ": " + msg), None) - case TypedErrorEvent(name, event, Some(msg), cause) => Summary(1, 0, 1, 0, Some(event + " - " + name + ": " + msg), cause) - case TypedErrorEvent(name, event, None, cause) => Summary(1, 0, 1, 0, Some(event + " - " + name), cause) - case ErrorEvent(msg) => Summary(1, 0, 1, 0, Some(msg), None) - case SpecificationReportEvent(successes, failures, errors, skipped, desc, systems, subSpecs) => Summary(successes + failures + errors + skipped, failures, errors, skipped, Some(desc), None) - case _ => {log.warn("Unrecognized error: " + event); Summary(1, 0, 1, 0, None, None)} - } - protected def open: Writer - protected def close = - { - onOut(_.close()) - out = None - } - def doInit = Control.trapAndLog(log){ out = Some(new PrintWriter(open)) } - def doComplete(finalResult: Result.Value) = - { - finalResult match - { - case Result.Error => println("Error during Tests") - case Result.Passed => println("All Tests Passed") - case Result.Failed => println("Tests Failed") - } - close - } - def doComplete(t: Throwable) = - { - println("Exception in Test Framework") - onOut(t.printStackTrace(_)) - close - } - def startGroup(name: String) = - { - groupCount = 0 - groupFailures = 0 - groupErrors = 0 - groupSkipped = 0 - groupMessages = Nil - } - def testEvent(event: TestEvent) = event.result match - { - case Some(result) => - { - val Summary(count, failures, errors, skipped, msg, cause) = result match - { - case Result.Passed => passedEventHandler(event) - case Result.Failed => failedEventHandler(event) - case Result.Error => errorEventHandler(event) - } - groupCount += count - groupFailures += failures - groupErrors += errors - groupSkipped += skipped - groupMessages ++= msg.toList - } - case None => {} - } - def endGroup(name: String, t: Throwable) = - { - groupMessages = Nil - println("Exception in " + name) - onOut(t.printStackTrace(_)) - } - def endGroup(name: String, result: Result.Value) = - { - result match - { - case Result.Error => println("Error: " + name + " - Count " + groupCount + ", Failed " + groupFailures + ", Errors " + groupErrors) - case Result.Passed => println("Passed: " + name + " - Count " + groupCount + ", Failed " + groupFailures + ", Errors " + groupErrors) - case Result.Failed => println("Failed: " + name + " - Count " + groupCount + ", Failed " + groupFailures + ", Errors " + groupErrors) - } - if(!groupMessages.isEmpty) - { - groupMessages.foreach(println(_)) - groupMessages = Nil - println("") - } - } - protected def onOut(f: PrintWriter => Unit) = Control.trapAndLog(log){ - out match - { - case Some(pw) => f(pw) - case None => log.warn("Method called when output was not open") - } - } - protected def println(s: String) = onOut(_.println(s)) -} - -class FileReportListener(val file: Path, log: Logger) extends WriterReportListener(log) -{ - def open = new java.io.FileWriter(file.asFile) -} - abstract class TestEvent extends NotNull { def result: Option[Result.Value] + def detail: Seq[TEvent] = Nil } - -sealed abstract class ScalaCheckEvent extends TestEvent -final case class PassedEvent(name: String, msg: String) extends ScalaCheckEvent { def result = Some(Result.Passed) } -final case class FailedEvent(name: String, msg: String) extends ScalaCheckEvent { def result = Some(Result.Failed) } - -sealed abstract class ScalaTestEvent(val result: Option[Result.Value]) extends TestEvent -final case class TypedEvent(name: String, `type`: String, msg: Option[String])(result: Option[Result.Value]) extends ScalaTestEvent(result) -final case class TypedErrorEvent(name: String, `type`: String, msg: Option[String], cause: Option[Throwable])(result: Option[Result.Value]) extends ScalaTestEvent(result) -final case class MessageEvent(msg: String) extends ScalaTestEvent(None) -final case class ErrorEvent(msg: String) extends ScalaTestEvent(None) -final case class IgnoredEvent(name: String, msg: Option[String]) extends ScalaTestEvent(Some(Result.Passed)) - -sealed abstract class SpecsEvent extends TestEvent -final case class SpecificationReportEvent(successes: Int, failures: Int, errors: Int, skipped: Int, pretty: String, systems: Seq[SystemReportEvent], subSpecs: Seq[SpecificationReportEvent]) extends SpecsEvent +object TestEvent { - def result = if(errors > 0) Some(Result.Error) else if(failures > 0) Some(Result.Failed) else Some(Result.Passed) -} -final case class SystemReportEvent(description: String, verb: String, skipped: Seq[Throwable], literateDescription: Option[Seq[String]], examples: Seq[ExampleReportEvent]) extends SpecsEvent { def result = None } -final case class ExampleReportEvent(description: String, errors: Seq[Throwable], failures: Seq[RuntimeException], skipped: Seq[RuntimeException], subExamples: Seq[ExampleReportEvent]) extends SpecsEvent { def result = None } - -trait EventOutput[E <: TestEvent] -{ - def output(e: E): Unit -} - -sealed abstract class LazyEventOutput[E <: TestEvent](val log: Logger) extends EventOutput[E] - -class ScalaCheckOutput(log: Logger) extends LazyEventOutput[ScalaCheckEvent](log) -{ - def output(event: ScalaCheckEvent) = event match - { - case PassedEvent(name, msg) => log.info("+ " + name + ": " + msg) - case FailedEvent(name, msg) => log.error("! " + name + ": " + msg) + def apply(events: Seq[TEvent]): TestEvent = + { + val overallResult = (Result.Passed /: events) { (sum, event) => + val result = event.result + if(sum == Result.Error || result == TResult.Error) Result.Error + else if(sum == Result.Failed || result == TResult.Failure) Result.Failed + else Result.Passed } -} - -class ScalaTestOutput(log: Logger) extends LazyEventOutput[ScalaTestEvent](log) -{ - def output(event: ScalaTestEvent) = event match - { - case TypedEvent(name, event, Some(msg)) => log.info(event + " - " + name + ": " + msg) - case TypedEvent(name, event, None) => log.info(event + " - " + name) - case TypedErrorEvent(name, event, Some(msg), cause) => logError(event + " - " + name + ": " + msg, cause) - case TypedErrorEvent(name, event, None, cause) => logError(event + " - " + name, cause) - case MessageEvent(msg) => log.info(msg) - case ErrorEvent(msg) => logError(msg, None) - case IgnoredEvent(name, Some(msg)) => log.info("Test ignored - " + name + ": " + msg) - case IgnoredEvent(name, None) => log.info("Test ignored - " + name) - } - private def logError(message: String, cause: Option[Throwable]) - { - cause.foreach(x => log.trace(x)) - log.error(message) - } -} - -class SpecsOutput(val log: Logger) extends EventOutput[SpecsEvent] -{ - private val Indent = " " - - def output(event: SpecsEvent) = event match - { - case sre: SpecificationReportEvent => reportSpecification(sre, "") - case sre: SystemReportEvent => reportSystem(sre, "") - case ere: ExampleReportEvent => reportExample(ere, "") - } - - /* The following is closely based on org.specs.runner.OutputReporter, - * part of specs, which is Copyright 2007-2008 Eric Torreborre. - * */ - - private def reportSpecification(specification: SpecificationReportEvent, padding: String) - { - val newIndent = padding + Indent - reportSpecifications(specification.subSpecs, newIndent) - reportSystems(specification.systems, newIndent) - } - private def reportSpecifications(specifications: Iterable[SpecificationReportEvent], padding: String) - { - for(specification <- specifications) - reportSpecification(specification, padding) - } - private def reportSystems(systems: Iterable[SystemReportEvent], padding: String) - { - for(system <- systems) - reportSystem(system, padding) - } - private def reportSystem(sus: SystemReportEvent, padding: String) - { - val skipped = if(sus.skipped.isEmpty) "" else sus.skipped.map(_.getMessage).mkString(" (skipped: ", ", ", ")") - log.info(padding + sus.description + " " + sus.verb + skipped) - for(description <- sus.literateDescription) - log.info(padding + description.mkString) - reportExamples(sus.examples, padding) - log.info(" ") - } - private def reportExamples(examples: Iterable[ExampleReportEvent], padding: String) - { - for(example <- examples) - { - reportExample(example, padding) - reportExamples(example.subExamples, padding + Indent) - } - } - private def status(example: ExampleReportEvent) = - { - if (example.errors.size + example.failures.size > 0) - "x " - else if (example.skipped.size > 0) - "o " - else - "+ " - } - private def reportExample(example: ExampleReportEvent, padding: String) - { - log.info(padding + status(example) + example.description) - for(skip <- example.skipped) - { - log.trace(skip) - log.warn(padding + skip.toString) - } - for(e <- example.failures ++ example.errors) - { - log.trace(e) - log.error(padding + e.toString) + new TestEvent { + val result = Some(overallResult) + override val detail = events } } } -class LogTestReportListener(val log: Logger) extends TestsListener +object TestLogger { - lazy val scalaCheckOutput: EventOutput[ScalaCheckEvent] = createScalaCheckOutput - lazy val scalaTestOutput: EventOutput[ScalaTestEvent] = createScalaTestOutput - lazy val specsOutput: EventOutput[SpecsEvent] = createSpecsOutput - - protected def createScalaCheckOutput = new ScalaCheckOutput(log) - protected def createScalaTestOutput = new ScalaTestOutput(log) - protected def createSpecsOutput = new SpecsOutput(log) + def apply(logger: sbt.Logger): TestLogger = new TestLogger(wrap(logger)) + def wrap(logger: sbt.Logger): TLogger = + new TLogger + { + def error(s: String) = log(Level.Error, s) + def warn(s: String) = log(Level.Warn, s) + def info(s: String) = log(Level.Info, s) + def debug(s: String) = log(Level.Debug, s) + private def log(level: Level.Value, s: String) = logger.log(level, s) + def ansiCodesSupported() = logger.ansiCodesSupported + } +} +class TestLogger(val log: TLogger) extends TestsListener +{ + protected var skipped, errors, passed, failures = 0 def startGroup(name: String) {} - def testEvent(event: TestEvent) - { - log.debug("in testEvent:" + event) - count(event) - event match - { - case sce: ScalaCheckEvent => scalaCheckOutput.output(sce) - case ste: ScalaTestEvent => scalaTestOutput.output(ste) - case se: SpecsEvent => specsOutput.output(se) - case e => handleOtherTestEvent(e) - } - } - protected def handleOtherTestEvent(event: TestEvent) {} + def testEvent(event: TestEvent): Unit = event.detail.foreach(count) def endGroup(name: String, t: Throwable) { log.error("Could not run test " + name + ": " + t.toString) - log.trace(t) + //log.trace(t) } - def endGroup(name: String, result: Result.Value) + def endGroup(name: String, result: Result.Value) {} + protected def count(event: TEvent): Unit = { - log.debug("in endGroup:" + result) - } - protected def count(event: TestEvent): Unit = - { - for(result <- event.result) + event.result match { - totalTests += 1 - result match - { - case Result.Error => errors += 1 - case Result.Failed => failed += 1 - case Result.Passed => passed += 1 - } + case TResult.Error => errors +=1 + case TResult.Success => passed +=1 + case TResult.Failure => failures +=1 + case TResult.Skipped => skipped += 1 } } - protected var totalTests, errors, passed, failed = 0 def doInit { - totalTests = 0 + failures = 0 errors = 0 passed = 0 - failed = 0 + skipped = 0 } /** called once, at end. */ def doComplete(finalResult: Result.Value): Unit = - log.info(Run: {totalTests.toString}, Passed: {passed.toString}, Errors: {errors.toString}, Failed: {failed.toString}.text) + { + val totalCount = failures + errors + skipped + passed + val postfix = ": Total " + totalCount + ", Failed " + failures + ", Errors " + errors + ", Passed " + passed + ", Skipped " + skipped + finalResult match + { + case Result.Error => log.error("Error" + postfix) + case Result.Passed => log.info("Passed: " + postfix) + case Result.Failed => log.error("Failed: " + postfix) + } + } + override def contentLogger: Option[TLogger] = Some(log) } diff --git a/src/main/scala/sbt/impl/TestFrameworkImpl.scala b/src/main/scala/sbt/impl/TestFrameworkImpl.scala deleted file mode 100755 index 06aa19699..000000000 --- a/src/main/scala/sbt/impl/TestFrameworkImpl.scala +++ /dev/null @@ -1,222 +0,0 @@ -/* sbt -- Simple Build Tool - * Copyright 2008, 2009 Steven Blundy, Mark Harrah - */ - -package sbt.impl - -import scala.xml.{Elem, Group} - -/* The following classes run tests for their associated test framework. -* NOTE #1: DO NOT actively use these classes. Only specify their names to LazyTestFramework -* for reflective loading. This allows using the test libraries provided on the -* project classpath instead of requiring global versions. -* NOTE #2: Keep all active uses of these test frameworks inside these classes so that sbt -* runs without error when a framework is not available at runtime and no tests for that -* framework are defined.*/ - -/** The test runner for ScalaCheck tests. */ -private[sbt] class ScalaCheckRunner(val log: Logger, val listeners: Seq[TestReportListener], val testLoader: ClassLoader) extends BasicTestRunner -{ - import org.scalacheck.{Pretty, Properties, Test} - def runTest(testClassName: String): Result.Value = - { - val test = ModuleUtilities.getObject(testClassName, testLoader).asInstanceOf[Properties] - if(Test.checkProperties(test, Test.defaultParams, propReport, testReport).find(!_._2.passed).isEmpty) - Result.Passed - else - Result.Failed - } - private def propReport(pName: String, s: Int, d: Int) {} - private def testReport(pName: String, res: Test.Result) = - { - if(res.passed) - fire(PassedEvent(pName, pretty(res))) - else - fire(FailedEvent(pName, pretty(res))) - } - private def pretty(res: Test.Result): String = - { - try { pretty1_5(res) } - catch { case e: NoSuchMethodError => pretty1_6(res) } - } - private def pretty1_5(res: Test.Result): String = Pretty.pretty(res) - private def pretty1_6(res: Test.Result): String = - { - // the following is equivalent to: (Pretty.prettyTestRes(res))(Pretty.defaultParams) - // and is necessary because of binary incompatibility in Pretty between ScalaCheck 1.5 and 1.6 - val loader = getClass.getClassLoader - val prettyObj = ModuleUtilities.getObject("org.scalacheck.Pretty", loader) - val prettyInst = prettyObj.getClass.getMethod("prettyTestRes", classOf[Test.Result]).invoke(prettyObj, res) - val defaultParams = prettyObj.getClass.getMethod("defaultParams").invoke(prettyObj) - prettyInst.getClass.getMethod("apply", Class.forName("org.scalacheck.Pretty$Params", true, loader)).invoke(prettyInst, defaultParams).toString - } -} -/** The test runner for ScalaTest suites. */ -private[sbt] class ScalaTestRunner(val log: Logger, val listeners: Seq[TestReportListener], val testLoader: ClassLoader) extends BasicTestRunner -{ - def runTest(testClassName: String): Result.Value = - { - import org.scalatest.{Stopper, Suite} - val testClass = Class.forName(testClassName, true, testLoader).asSubclass(classOf[Suite]) - val test = testClass.newInstance - val reporter = new ScalaTestReporter - val stopper = new Stopper { override def stopRequested = false } - test.execute(None, reporter, stopper, Set.empty, Set("org.scalatest.Ignore"), Map.empty, None) - if(reporter.succeeded) - Result.Passed - else - Result.Failed - } - - /** An implementation of Reporter for ScalaTest. */ - private class ScalaTestReporter extends org.scalatest.Reporter with NotNull - { - import org.scalatest.Report - override def testIgnored(report: Report) = - { - if(report.message.trim.isEmpty) fire(IgnoredEvent(report.name, None)) - else fire(IgnoredEvent(report.name, Some(report.message.trim))) - } - override def testStarting(report: Report) { info(report, "Test starting", None) } - override def testSucceeded(report: Report) { info(report, "Test succeeded", Some(Result.Passed)) } - override def testFailed(report: Report) - { - succeeded = false - error(report, "Test failed", Some(Result.Failed)) - } - - override def infoProvided(report : Report) { info(report, "", None) } - - override def suiteStarting(report: Report) { info(report, "Suite starting", None) } - override def suiteCompleted(report: Report) { info(report, "Suite completed", None) } - override def suiteAborted(report: Report) { error(report, "Suite aborted", None) } - - override def runStarting(testCount: Int) { fire(MessageEvent("Run starting")) } - override def runStopped() - { - succeeded = false - fire(ErrorEvent("Run stopped")) - } - override def runAborted(report: Report) - { - succeeded = false - error(report, "Run aborted", None) - } - override def runCompleted() { log.info("Run completed.") } - - private def error(report: Report, event: String, result: Option[Result.Value]) { logReport(report, event, result, Level.Error) } - private def info(report: Report, event: String, result: Option[Result.Value]) { logReport(report, event, result, Level.Info) } - private def logReport(report: Report, event: String, result: Option[Result.Value], level: Level.Value) - { - level match - { - case Level.Error => - if(report.message.trim.isEmpty) - fire(TypedErrorEvent(report.name, event, None, report.throwable)(result)) - else - fire(TypedErrorEvent(report.name, event, Some(report.message.trim), report.throwable)(result)) - case Level.Info => - if(report.message.trim.isEmpty) - fire(TypedEvent(report.name, event, None)(result)) - else - fire(TypedEvent(report.name, event, Some(report.message.trim))(result)) - case l => log.warn("Level not expected:" + l) - } - } - - var succeeded = true - } -} -/** The test runner for specs tests. */ -private[sbt] class SpecsRunner(val log: Logger, val listeners: Seq[TestReportListener], val testLoader: ClassLoader) extends BasicTestRunner -{ - import org.specs.Specification - import org.specs.specification.{Example, Sus} - - def runTest(testClassName: String): Result.Value = - { - val test = ModuleUtilities.getObject(testClassName, testLoader).asInstanceOf[Specification] - val event = reportSpecification(test) - fire(event) - if(test.isFailing) - Result.Failed - else - Result.Passed - } - - /* The following is closely based on org.specs.runner.OutputReporter, - * part of specs, which is Copyright 2007-2008 Eric Torreborre. - * */ - - private def reportSpecification(spec: Specification): SpecificationReportEvent = - { - // this is for binary compatibility between specs 1.4.x and 1.5.0: the ancestor of Specification containing these two methods changed - def reflectSeq[T](name: String) = classOf[Specification].getMethod(name).invoke(spec).asInstanceOf[Seq[T]] - val systems = reflectSeq[Sus]("systems") - val subSpecifications = reflectSeq[Specification]("subSpecifications") - - return SpecificationReportEvent(spec.successes.size, spec.failures.size, spec.errors.size, spec.skipped.size, spec.pretty, - reportSystems(systems), reportSpecifications(subSpecifications)) - } - private def reportSpecifications(specifications: Seq[Specification]): Seq[SpecificationReportEvent] = - { - for(specification <- specifications) yield - reportSpecification(specification) - } - private def reportSystems(systems: Seq[Sus]): Seq[SystemReportEvent] = - { - for(system <- systems) yield - reportSystem(system) - } - private def reportSystem(sus: Sus): SystemReportEvent = - { - // for source compatibility between specs 1.4.x and 1.5.0: - // in specs 1.5.0, description is LiterateDescription - // in specs < 1.5.0, description is Elem - // LiterateDescription.desc is a Node - // Elem.child is a Seq[Node] - // each has a map[T](f: Node => T): Seq[T] defined so we use reflection to call the right method - - //description.child.map(_.text) // Elem equivalent - //description.desc.map(_.text) // LiterateDescription - def formatDescription(a: AnyRef) = - { - val toMap = - try { call(a, "desc") } - catch { case e: Exception => call(a, "child") } - val mapText = (a: AnyRef) => a.getClass.getMethod("text").invoke(a) - toMap.getClass.getMethod("map", Class.forName("scala.Function1")).invoke(toMap, mapText).asInstanceOf[Seq[String]] - } - def call(a: AnyRef, m: String) = a.getClass.getMethod(m).invoke(a) - def format: Option[Seq[String]] = - { - val litD = sus.literateDescription - if(litD.isEmpty) None else Some(formatDescription(litD.get)) - } - // these are for 1.6 compatibility, which removed skippedSus (skipped still exists) and examples (moved to specification) - def skipped(sus: Sus) = classOf[Sus].getMethod("skipped").invoke(sus).asInstanceOf[Seq[Throwable]] - def examples(sus: Sus) = - { - try { sus.examples } // we compile against specs 1.4.x, which has examples directly on Sus so this compiles - catch { case _: NoSuchMethodError => // It fails at runtime for specs 1.6 because examples is now on BaseSpecification - val spec = classOf[Sus].getMethod("specification").invoke(sus) - spec.getClass.getMethod("examples").invoke(spec).asInstanceOf[List[Example]] - } - } - SystemReportEvent(sus.description, sus.verb, skipped(sus), format, reportExamples(examples(sus))) - } - private def reportExamples(examples: Seq[Example]): Seq[ExampleReportEvent] = - { - for(example <- examples) yield - reportExample(example) - } - private def reportExample(example: Example): ExampleReportEvent = - { - def examples(example: Example) = - try { example.subExamples } // we compile against specs 1.4.x, which has subExamples defined on Example, so this compiles - catch { case _ : NoSuchMethodError => // It fails at runtime for specs 1.6 because examples is the new method - classOf[Example].getMethod("examples").invoke(example).asInstanceOf[Seq[Example]] - } - ExampleReportEvent(example.description, example.errors, example.failures, example.skipped, reportExamples(examples(example))) - } -} diff --git a/src/main/scala/sbt/impl/TestParser.scala b/src/main/scala/sbt/impl/TestParser.scala index 69c7b5c72..d12bc9319 100644 --- a/src/main/scala/sbt/impl/TestParser.scala +++ b/src/main/scala/sbt/impl/TestParser.scala @@ -15,7 +15,7 @@ import scala.util.parsing.combinator._ import TestParser._ /** Represents a test implemented by 'testClassName' of type 'superClassName'.*/ -final case class TestDefinition(isModule: Boolean, testClassName: String, superClassName: String) extends NotNull +final case class TestDefinition(isModule: Boolean, testClassName: String, superClassName: String) extends org.scalatools.testing.TestFingerprint with NotNull { override def toString = (if(isModule) IsModuleLiteral else "") + testClassName + SubSuperSeparator + superClassName