merge test-quick reimplementation #393 from vigdorchik/wip_test_quick

This commit is contained in:
Mark Harrah 2012-03-09 09:31:39 -05:00
commit 4527e9bfc6
4 changed files with 101 additions and 99 deletions

78
main/Defaults.scala Normal file → Executable file
View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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))
}
}