diff --git a/main/Defaults.scala b/main/Defaults.scala index d62f3e913..ca0e66370 100644 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -64,11 +64,8 @@ object Defaults extends BuildCommon logBuffered :== false, connectInput :== false, cancelable :== false, - cancelable :== false, autoScalaLibrary :== true, onLoad <<= onLoad ?? idFun[State], - tags in test := Seq(Tags.Test -> 1), - tags in testOnly <<= tags in test, onUnload <<= (onUnload ?? idFun[State]), onUnload <<= (onUnload, taskTemporaryDirectory) { (f, dir) => s => { try f(s) finally IO.delete(dir) } }, watchingMessage <<= watchingMessage ?? Watched.defaultWatchingMessage, @@ -78,8 +75,6 @@ object Defaults extends BuildCommon trapExit in run :== true, traceLevel in run :== 0, traceLevel in runMain :== 0, - logBuffered in testOnly :== true, - logBuffered in test :== true, traceLevel in console :== Int.MaxValue, traceLevel in consoleProject :== Int.MaxValue, autoCompilerPlugins :== true, @@ -121,7 +116,13 @@ object Defaults extends BuildCommon includeFilter in unmanagedResources :== AllPassFilter, excludeFilter :== (".*" - ".") || HiddenFileFilter, pomIncludeRepository :== Classpaths.defaultRepositoryFilter - )) + ) ++ { + val testSettings = for (task <- Seq(test, testOnly, testQuick)) yield Seq[Setting[_]]( + logBuffered in task := true, + tags in task := Seq(Tags.Test -> 1) + ) + testSettings.flatten + }) def projectCore: Seq[Setting[_]] = Seq( name <<= thisProject(_.id), logManager <<= extraLoggers(LogManager.defaults), @@ -285,16 +286,18 @@ object Defaults extends BuildCommon definedTestNames <<= definedTests map ( _.map(_.name).distinct) storeAs definedTestNames triggeredBy compile, testListeners in GlobalScope :== Nil, testOptions in GlobalScope :== Nil, - testExecution in test <<= testExecutionTask(test), - testExecution in testOnly <<= testExecutionTask(testOnly), testFilter in testOnly :== (selectedFilter _), + testFilter in testQuick <<= testQuickFilter, executeTests <<= (streams in test, loadedTestFrameworks, testExecution in test, testLoader, definedTests, resolvedScoped, state) flatMap { (s, frameworkMap, config, loader, discovered, scoped, st) => implicit val display = Project.showContextKey(st) Tests(frameworkMap, loader, discovered, config, noTestsMessage(ScopedKey(scoped.scope, test.key)), s.log) }, test <<= (executeTests, streams) map { (results, s) => Tests.showResults(s.log, results) }, - testOnly <<= testOnlyTask + testOnly <<= inputTests(testOnly), + testQuick <<= inputTests(testQuick) + ) ++ ( + for (task <- Seq(test, testOnly, testQuick)) yield testExecution in task <<= testExecutionTask(task) ) private[this] def noTestsMessage(scoped: ScopedKey[_])(implicit display: Show[ScopedKey[_]]): String = "No tests to run for " + display(scoped) @@ -324,7 +327,14 @@ object Defaults extends BuildCommon def testExecutionTask(task: Scoped): Initialize[Task[Tests.Execution]] = (testOptions in task, parallelExecution in task, tags in task) map { (opts, par, ts) => new Tests.Execution(opts, par, ts) } - def testOnlyTask: Initialize[InputTask[Unit]] = inputTests(testOnly) + def testQuickFilter: Initialize[Task[Seq[String] => String => Boolean]] = + (compile in test, cacheDirectory) map { + case (analysis, dir) => + val succeeded = new TestResultFilter(dir / "succeeded_tests") + args => test => selectedFilter(args)(test) && { + !succeeded(test) // Add recompilation status. + } + } def inputTests(key: InputKey[_]): Initialize[InputTask[Unit]] = InputTask( loadForParser(definedTestNames)( (s, i) => testOnlyParser(s, i getOrElse Nil) ) ) { result => diff --git a/main/Keys.scala b/main/Keys.scala index c5ba0fa9c..e235f8b42 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -192,6 +192,7 @@ object Keys val executeTests = TaskKey[Tests.Output]("execute-tests", "Executes all tests, producing a report.") val test = TaskKey[Unit]("test", "Executes all tests.") val testOnly = InputKey[Unit]("test-only", "Executes the tests provided as arguments or all tests if no arguments are provided.") + val testQuick = InputKey[Unit]("test-quick", "Executes the tests that either failed before, were not run or whose transitive dependencies changed, among those provided as arguments.") val testOptions = TaskKey[Seq[TestOption]]("test-options", "Options for running tests.") val testFrameworks = SettingKey[Seq[TestFramework]]("test-frameworks", "Registered, although not necessarily present, test frameworks.") val testListeners = TaskKey[Seq[TestReportListener]]("test-listeners", "Defines test listeners.") diff --git a/testing/TestStatusReporter.scala b/testing/TestStatusReporter.scala new file mode 100644 index 000000000..f619d9717 --- /dev/null +++ b/testing/TestStatusReporter.scala @@ -0,0 +1,51 @@ +/* sbt -- Simple Build Tool + * Copyright 2009, 2010, 2011, 2012 Mark Harrah + */ +package sbt + +import java.io.File + +import scala.collection.mutable.Map + +// Assumes exclusive ownership of the file. +private[sbt] class TestStatusReporter(f: File) extends TestsListener +{ + private lazy val succeeded = TestStatus.read(f) + + def doInit {} + def startGroup(name: String) { succeeded remove name } + def testEvent(event: TestEvent) {} + def endGroup(name: String, t: Throwable) {} + def endGroup(name: String, result: TestResult.Value) { + if(result == TestResult.Passed) + succeeded(name) = System.currentTimeMillis + } + def doComplete(finalResult: TestResult.Value) { + TestStatus.write(succeeded, "Successful Tests", f) + } +} + +private[sbt] class TestResultFilter(f: File) extends (String => Boolean) with NotNull +{ + private lazy val succeeded = TestStatus.read(f) + def apply(test: String) = succeeded.contains(test) +} + +private object TestStatus +{ + import java.util.Properties + def read(f: File): Map[String, Long] = + { + import scala.collection.JavaConversions.{enumerationAsScalaIterator, propertiesAsScalaMap} + val properties = new Properties + IO.load(properties, f) + properties map {case (k, v) => (k, v.toLong)} + } + def write(map: Map[String, Long], label: String, f: File) + { + val properties = new Properties + for( (test, lastSuccessTime) <- map) + properties.setProperty(test, lastSuccessTime.toString) + IO.write(properties, label, f) + } +} diff --git a/testing/impl/TestStatusReporter.scala b/testing/impl/TestStatusReporter.scala deleted file mode 100644 index 0b83989a7..000000000 --- a/testing/impl/TestStatusReporter.scala +++ /dev/null @@ -1,75 +0,0 @@ -/* sbt -- Simple Build Tool - * Copyright 2009 Mark Harrah - */ -package sbt.impl -import sbt._ - -import java.io.File -import scala.collection.mutable.{HashMap, Map} - -/** Only intended to be used once per instance. */ -private[sbt] class TestStatusReporter(path: Path, log: Logger) extends TestsListener -{ - private lazy val succeeded: Map[String, Long] = TestStatus.read(path, log) - - def doInit {} - def startGroup(name: String) { succeeded remove name } - def testEvent(event: TestEvent) {} - def endGroup(name: String, t: Throwable) {} - def endGroup(name: String, result: Result.Value) - { - if(result == Result.Passed) - succeeded(name) = System.currentTimeMillis - } - def doComplete(finalResult: Result.Value) { complete() } - def doComplete(t: Throwable) { complete() } - - private def complete() - { - TestStatus.write(succeeded, "Successful Tests", path, log) - } -} - -private[sbt] class TestQuickFilter(testAnalysis: CompileAnalysis, failedOnly: Boolean, path: Path, log: Logger) extends (String => Boolean) with NotNull -{ - private lazy val exclude = TestStatus.read(path, log) - private lazy val map = testAnalysis.testSourceMap - def apply(test: String) = - exclude.get(test) match - { - case None => true // include because this test has not been run or did not succeed - case Some(lastSuccessTime) => // succeeded the last time it was run - if(failedOnly) - false // don't include because the last time succeeded - else - testAnalysis.products(map(test)) match - { - case None => true - case Some(products) => products.exists(lastSuccessTime <= _.lastModified) // include if the test is newer than the last run - } - } -} -private object TestStatus -{ - import java.util.Properties - def read(path: Path, log: Logger): Map[String, Long] = - { - val map = new HashMap[String, Long] - val properties = new Properties - logError(PropertiesUtilities.load(properties, path, log), "loading", log) - for(test <- PropertiesUtilities.propertyNames(properties)) - map.put(test, properties.getProperty(test).toLong) - map - } - def write(map: Map[String, Long], label: String, path: Path, log: Logger) - { - val properties = new Properties - for( (test, lastSuccessTime) <- map) - properties.setProperty(test, lastSuccessTime.toString) - logError(PropertiesUtilities.write(properties, label, path, log), "writing", log) - } - private def logError(result: Option[String], action: String, log: Logger) - { - result.foreach(msg => log.error("Error " + action + " test status: " + msg)) - } -} \ No newline at end of file