diff --git a/main/Defaults.scala b/main/Defaults.scala old mode 100644 new mode 100755 index 47b792e6a..000355cbe --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -50,7 +50,7 @@ object Defaults extends BuildCommon def thisBuildCore: Seq[Setting[_]] = inScope(GlobalScope.copy(project = Select(ThisBuild)))(Seq( managedDirectory <<= baseDirectory(_ / "lib_managed") )) - def globalCore: Seq[Setting[_]] = inScope(GlobalScope)(Seq( + def globalCore: Seq[Setting[_]] = inScope(GlobalScope)(defaultTestTasks(test) ++ defaultTestTasks(testOnly) ++ defaultTestTasks(testQuick) ++ Seq( crossVersion :== CrossVersion.Disabled, buildDependencies <<= buildDependencies or Classpaths.constructBuildDependencies, taskTemporaryDirectory := IO.createTemporaryDirectory, @@ -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, @@ -122,6 +117,10 @@ object Defaults extends BuildCommon excludeFilter :== (".*" - ".") || HiddenFileFilter, pomIncludeRepository :== Classpaths.defaultRepositoryFilter )) + def defaultTestTasks(key: Scoped): Seq[Setting[_]] = Seq( + tags in key := Seq(Tags.Test -> 1), + logBuffered in key := true + ) def projectCore: Seq[Setting[_]] = Seq( name <<= thisProject(_.id), logManager <<= extraLoggers(LogManager.defaults), @@ -272,7 +271,7 @@ object Defaults extends BuildCommon } } - lazy val testTasks: Seq[Setting[_]] = testTaskOptions(test) ++ testTaskOptions(testOnly) ++ Seq( + lazy val testTasks: Seq[Setting[_]] = testTaskOptions(test) ++ testTaskOptions(testOnly) ++ testTaskOptions(testQuick) ++ Seq( testLoader <<= (fullClasspath, scalaInstance, taskTemporaryDirectory) map { (cp, si, temp) => TestFramework.createTestLoader(data(cp), si, IO.createUniqueDirectory(temp)) }, testFrameworks in GlobalScope :== { import sbt.TestFrameworks._ @@ -285,15 +284,16 @@ 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) ) private[this] def noTestsMessage(scoped: ScopedKey[_])(implicit display: Show[ScopedKey[_]]): String = "No tests to run for " + display(scoped) @@ -301,10 +301,11 @@ object Defaults extends BuildCommon lazy val TaskGlobal: Scope = ThisScope.copy(task = Global) lazy val ConfigGlobal: Scope = ThisScope.copy(config = Global) def testTaskOptions(key: Scoped): Seq[Setting[_]] = inTask(key)( Seq( - testListeners <<= (streams, resolvedScoped, streamsManager, logBuffered, testListeners in TaskGlobal) map { (s, sco, sm, buff, ls) => - TestLogger(s.log, testLogger(sm, test in sco.scope), buff) +: ls + testListeners <<= (streams, resolvedScoped, streamsManager, logBuffered, cacheDirectory in test, testListeners in TaskGlobal) map { (s, sco, sm, buff, dir, ls) => + TestLogger(s.log, testLogger(sm, test in sco.scope), buff) +: new TestStatusReporter(succeededFile(dir)) +: ls }, - testOptions <<= (testOptions in TaskGlobal, testListeners) map { (options, ls) => Tests.Listeners(ls) +: options } + testOptions <<= (testOptions in TaskGlobal, testListeners) map { (options, ls) => Tests.Listeners(ls) +: options }, + testExecution <<= testExecutionTask(key) ) ) def testLogger(manager: Streams, baseKey: Scoped)(tdef: TestDefinition): Logger = { @@ -323,19 +324,48 @@ 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 = - InputTask( loadForParser(definedTestNames)( (s, i) => testOnlyParser(s, i getOrElse Nil) ) ) { result => - (streams, loadedTestFrameworks, testExecution in testOnly, testLoader, definedTests, resolvedScoped, result, state) flatMap { - case (s, frameworks, config, loader, discovered, scoped, (tests, frameworkOptions), st) => - val filter = selectedFilter(tests) - val modifiedOpts = Tests.Filter(filter) +: Tests.Argument(frameworkOptions : _*) +: config.options - val newConfig = new Tests.Execution(modifiedOpts, config.parallel, config.tags) - implicit val display = Project.showContextKey(st) - Tests(frameworks, loader, discovered, newConfig, noTestsMessage(scoped), s.log) map { results => - Tests.showResults(s.log, results) + def testQuickFilter: Initialize[Task[Seq[String] => String => Boolean]] = + (fullClasspath in test, cacheDirectory) map { + (cp, dir) => + val ans = cp.flatMap(_.metadata get Keys.analysis) + val succeeded = TestStatus.read(succeededFile(dir)) + val stamps = collection.mutable.Map.empty[File, Long] + def stamp(dep: String): Long = { + val stamps = for (a <- ans; f <- a.relations.definesClass(dep)) yield intlStamp(f, a, Set.empty) + if (stamps.isEmpty) Long.MinValue else stamps.max + } + def intlStamp(f: File, analysis: inc.Analysis, s: Set[File]): Long = { + if (s contains f) Long.MinValue else + stamps.getOrElseUpdate(f, { + import analysis.{relations => rel, apis} + rel.internalSrcDeps(f).map(intlStamp(_, analysis, s + f)) ++ + rel.externalDeps(f).map(stamp) + + apis.internal(f).compilation.startTime + }.max) + } + args => test => selectedFilter(args)(test) && { + succeeded.get(test) match { + case None => true + case Some(ts) => stamp(test) > ts + } } } - } + def succeededFile(dir: File) = dir / "succeeded_tests" + + def inputTests(key: InputKey[_]): Initialize[InputTask[Unit]] = + InputTask( loadForParser(definedTestNames)( (s, i) => testOnlyParser(s, i getOrElse Nil) ) ) { result => + (streams, loadedTestFrameworks, testFilter in key, testExecution in key, testLoader, definedTests, resolvedScoped, result, state) flatMap { + case (s, frameworks, filter, config, loader, discovered, scoped, (tests, frameworkOptions), st) => + val modifiedOpts = Tests.Filter(filter(tests)) +: Tests.Argument(frameworkOptions : _*) +: config.options + val newConfig = new Tests.Execution(modifiedOpts, config.parallel, config.tags) + implicit val display = Project.showContextKey(st) + Tests(frameworks, loader, discovered, newConfig, noTestsMessage(scoped), s.log) map { results => + Tests.showResults(s.log, results) + } + } + } + + def selectedFilter(args: Seq[String]): String => Boolean = { val filters = args map GlobFilter.apply diff --git a/main/Keys.scala b/main/Keys.scala index eeed6e3a9..e235f8b42 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -192,10 +192,12 @@ 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.") val testExecution = TaskKey[Tests.Execution]("test-execution", "Settings controlling test execution") + val testFilter = TaskKey[Seq[String] => String => Boolean]("test-filter", "Filter controlling whether the test is executed") val isModule = AttributeKey[Boolean]("is-module", "True if the target is a module.") // Classpath/Dependency Management Keys diff --git a/testing/TestStatusReporter.scala b/testing/TestStatusReporter.scala new file mode 100644 index 000000000..bab1d4b71 --- /dev/null +++ b/testing/TestStatusReporter.scala @@ -0,0 +1,45 @@ +/* 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] 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