mirror of https://github.com/sbt/sbt.git
Added TestResultLogger for customising test feedback.
1) When `test` is run and there are no tests available, omit logging output. Especially useful for aggregate modules. `test-only` et al unaffected. (#1185) 2) Added a new setting `testResultLogger` to allow customisation of logging of test results.
This commit is contained in:
parent
78d2aabda2
commit
195129a3e7
|
|
@ -0,0 +1,159 @@
|
||||||
|
package sbt
|
||||||
|
|
||||||
|
import sbt.Tests.{Output, Summary}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs information about tests after they finish.
|
||||||
|
*
|
||||||
|
* Log output can be customised by providing a specialised instance of this
|
||||||
|
* trait via the `testTestResultLogger` setting.
|
||||||
|
*
|
||||||
|
* @since 0.13.5
|
||||||
|
*/
|
||||||
|
trait TestResultLogger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform logging.
|
||||||
|
*
|
||||||
|
* @param log The target logger to write output to.
|
||||||
|
* @param results The test results about which to log.
|
||||||
|
* @param taskName The task about which we are logging. Eg. "my-module-b/test:test"
|
||||||
|
*/
|
||||||
|
def run(log: Logger, results: Output, taskName: String): Unit
|
||||||
|
|
||||||
|
/** Only allow invocation if certain criteria is met, else use another `TestResultLogger` (defaulting to nothing) . */
|
||||||
|
final def onlyIf(f: (Output, String) => Boolean, otherwise: TestResultLogger = TestResultLogger.Null) =
|
||||||
|
TestResultLogger.choose(f, this, otherwise)
|
||||||
|
|
||||||
|
/** Allow invocation unless a certain predicate passes, in which case use another `TestResultLogger` (defaulting to nothing) . */
|
||||||
|
final def unless(f: (Output, String) => Boolean, otherwise: TestResultLogger = TestResultLogger.Null) =
|
||||||
|
TestResultLogger.choose(f, otherwise, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
object TestResultLogger {
|
||||||
|
|
||||||
|
/** A `TestResultLogger` that does nothing. */
|
||||||
|
val Null = const(_ => ())
|
||||||
|
|
||||||
|
/** SBT's default `TestResultLogger`. Use `copy()` to change selective portions. */
|
||||||
|
val Default = Defaults.Main()
|
||||||
|
|
||||||
|
/** Twist on the default which is completely silent when the subject module doesn't contain any tests. */
|
||||||
|
def SilentWhenNoTests = silenceWhenNoTests(Default)
|
||||||
|
|
||||||
|
/** Creates a `TestResultLogger` using a given function. */
|
||||||
|
def apply(f: (Logger, Output, String) => Unit): TestResultLogger =
|
||||||
|
new TestResultLogger {
|
||||||
|
override def run(log: Logger, results: Output, taskName: String) =
|
||||||
|
f(log, results, taskName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a `TestResultLogger` that ignores its input and always performs the same logging. */
|
||||||
|
def const(f: Logger => Unit) = apply((l,_,_) => f(l))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a `TestResultLogger` based on a given predicate.
|
||||||
|
*
|
||||||
|
* @param t The `TestResultLogger` to choose if the predicate passes.
|
||||||
|
* @param f The `TestResultLogger` to choose if the predicate fails.
|
||||||
|
*/
|
||||||
|
def choose(cond: (Output, String) => Boolean, t: TestResultLogger, f: TestResultLogger) =
|
||||||
|
TestResultLogger((log, results, taskName) =>
|
||||||
|
(if (cond(results, taskName)) t else f).run(log, results, taskName))
|
||||||
|
|
||||||
|
/** Transforms the input to be completely silent when the subject module doesn't contain any tests. */
|
||||||
|
def silenceWhenNoTests(d: Defaults.Main) =
|
||||||
|
d.copy(
|
||||||
|
printStandard = d.printStandard.unless((results, _) => results.events.isEmpty),
|
||||||
|
printNoTests = Null
|
||||||
|
)
|
||||||
|
|
||||||
|
object Defaults {
|
||||||
|
|
||||||
|
/** SBT's default `TestResultLogger`. Use `copy()` to change selective portions. */
|
||||||
|
case class Main(
|
||||||
|
printStandard_? : Output => Boolean = Defaults.printStandard_?,
|
||||||
|
printSummary : TestResultLogger = Defaults.printSummary,
|
||||||
|
printStandard : TestResultLogger = Defaults.printStandard,
|
||||||
|
printFailures : TestResultLogger = Defaults.printFailures,
|
||||||
|
printNoTests : TestResultLogger = Defaults.printNoTests
|
||||||
|
) extends TestResultLogger {
|
||||||
|
|
||||||
|
override def run(log: Logger, results: Output, taskName: String): Unit = {
|
||||||
|
def run(r: TestResultLogger): Unit = r.run(log, results, taskName)
|
||||||
|
|
||||||
|
run(printSummary)
|
||||||
|
|
||||||
|
if (printStandard_?(results))
|
||||||
|
run(printStandard)
|
||||||
|
|
||||||
|
if (results.events.isEmpty)
|
||||||
|
run(printNoTests)
|
||||||
|
else
|
||||||
|
run(printFailures)
|
||||||
|
|
||||||
|
results.overall match {
|
||||||
|
case TestResult.Error | TestResult.Failed => throw new TestsFailedException
|
||||||
|
case TestResult.Passed =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val printSummary = TestResultLogger((log, results, _) => {
|
||||||
|
val multipleFrameworks = results.summaries.size > 1
|
||||||
|
for (Summary(name, message) <- results.summaries)
|
||||||
|
if(message.isEmpty)
|
||||||
|
log.debug("Summary for " + name + " not available.")
|
||||||
|
else {
|
||||||
|
if(multipleFrameworks) log.info(name)
|
||||||
|
log.info(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val printStandard_? : Output => Boolean =
|
||||||
|
results =>
|
||||||
|
// Print the standard one-liner statistic if no framework summary is defined, or when > 1 framework is in used.
|
||||||
|
results.summaries.size > 1 || results.summaries.headOption.forall(_.summaryText.size == 0)
|
||||||
|
|
||||||
|
val printStandard = TestResultLogger((log, results, _) => {
|
||||||
|
val (skippedCount, errorsCount, passedCount, failuresCount, ignoredCount, canceledCount, pendingCount) =
|
||||||
|
results.events.foldLeft((0, 0, 0, 0, 0, 0, 0)) { case ((skippedAcc, errorAcc, passedAcc, failureAcc, ignoredAcc, canceledAcc, pendingAcc), (name, testEvent)) =>
|
||||||
|
(skippedAcc + testEvent.skippedCount, errorAcc + testEvent.errorCount, passedAcc + testEvent.passedCount, failureAcc + testEvent.failureCount,
|
||||||
|
ignoredAcc + testEvent.ignoredCount, canceledAcc + testEvent.canceledCount, pendingAcc + testEvent.pendingCount)
|
||||||
|
}
|
||||||
|
val totalCount = failuresCount + errorsCount + skippedCount + passedCount
|
||||||
|
val base = s"Total $totalCount, Failed $failuresCount, Errors $errorsCount, Passed $passedCount"
|
||||||
|
|
||||||
|
val otherCounts = Seq("Skipped" -> skippedCount, "Ignored" -> ignoredCount, "Canceled" -> canceledCount, "Pending" -> pendingCount)
|
||||||
|
val extra = otherCounts.filter(_._2 > 0).map{case(label,count) => s", $label $count" }
|
||||||
|
|
||||||
|
val postfix = base + extra.mkString
|
||||||
|
results.overall match {
|
||||||
|
case TestResult.Error => log.error("Error: " + postfix)
|
||||||
|
case TestResult.Passed => log.info("Passed: " + postfix)
|
||||||
|
case TestResult.Failed => log.error("Failed: " + postfix)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val printFailures = TestResultLogger((log, results, _) => {
|
||||||
|
def select(resultTpe: TestResult.Value) = results.events collect {
|
||||||
|
case (name, tpe) if tpe.result == resultTpe =>
|
||||||
|
scala.reflect.NameTransformer.decode(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
def show(label: String, level: Level.Value, tests: Iterable[String]): Unit =
|
||||||
|
if (!tests.isEmpty) {
|
||||||
|
log.log(level, label)
|
||||||
|
log.log(level, tests.mkString("\t", "\n\t", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
show("Passed tests:", Level.Debug, select(TestResult.Passed))
|
||||||
|
show("Failed tests:", Level.Error, select(TestResult.Failed))
|
||||||
|
show("Error during tests:", Level.Error, select(TestResult.Error))
|
||||||
|
})
|
||||||
|
|
||||||
|
val printNoTests = TestResultLogger((log, results, taskName) =>
|
||||||
|
log.info("No tests to run for " + taskName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -264,78 +264,10 @@ object Tests
|
||||||
(tests, mains.toSet)
|
(tests, mains.toSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@deprecated("Tests.showResults() has been superseded with TestResultLogger and setting 'testResultLogger'.", "0.13.5")
|
||||||
def showResults(log: Logger, results: Output, noTestsMessage: =>String): Unit =
|
def showResults(log: Logger, results: Output, noTestsMessage: =>String): Unit =
|
||||||
{
|
TestResultLogger.Default.copy(printNoTests = TestResultLogger.const(_ info noTestsMessage))
|
||||||
val multipleFrameworks = results.summaries.size > 1
|
.run(log, results, "")
|
||||||
def printSummary(name: String, message: String)
|
|
||||||
{
|
|
||||||
if(message.isEmpty)
|
|
||||||
log.debug("Summary for " + name + " not available.")
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if(multipleFrameworks) log.info(name)
|
|
||||||
log.info(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Summary(name, messages) <- results.summaries)
|
|
||||||
printSummary(name, messages)
|
|
||||||
val noSummary = results.summaries.headOption.forall(_.summaryText.size == 0)
|
|
||||||
val printStandard = multipleFrameworks || noSummary
|
|
||||||
// Print the standard one-liner statistic if no framework summary is defined, or when > 1 framework is in used.
|
|
||||||
if (printStandard)
|
|
||||||
{
|
|
||||||
val (skippedCount, errorsCount, passedCount, failuresCount, ignoredCount, canceledCount, pendingCount) =
|
|
||||||
results.events.foldLeft((0, 0, 0, 0, 0, 0, 0)) { case ((skippedAcc, errorAcc, passedAcc, failureAcc, ignoredAcc, canceledAcc, pendingAcc), (name, testEvent)) =>
|
|
||||||
(skippedAcc + testEvent.skippedCount, errorAcc + testEvent.errorCount, passedAcc + testEvent.passedCount, failureAcc + testEvent.failureCount,
|
|
||||||
ignoredAcc + testEvent.ignoredCount, canceledAcc + testEvent.canceledCount, pendingAcc + testEvent.pendingCount)
|
|
||||||
}
|
|
||||||
val totalCount = failuresCount + errorsCount + skippedCount + passedCount
|
|
||||||
val base = s"Total $totalCount, Failed $failuresCount, Errors $errorsCount, Passed $passedCount"
|
|
||||||
|
|
||||||
val otherCounts = Seq("Skipped" -> skippedCount, "Ignored" -> ignoredCount, "Canceled" -> canceledCount, "Pending" -> pendingCount)
|
|
||||||
val extra = otherCounts.filter(_._2 > 0).map{case(label,count) => s", $label $count" }
|
|
||||||
|
|
||||||
val postfix = base + extra.mkString
|
|
||||||
results.overall match {
|
|
||||||
case TestResult.Error => log.error("Error: " + postfix)
|
|
||||||
case TestResult.Passed => log.info("Passed: " + postfix)
|
|
||||||
case TestResult.Failed => log.error("Failed: " + postfix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Let's always print out Failed tests for now
|
|
||||||
if (results.events.isEmpty)
|
|
||||||
log.info(noTestsMessage)
|
|
||||||
else {
|
|
||||||
import TestResult.{Error, Failed, Passed}
|
|
||||||
import scala.reflect.NameTransformer.decode
|
|
||||||
|
|
||||||
def select(resultTpe: TestResult.Value) = results.events collect {
|
|
||||||
case (name, tpe) if tpe.result == resultTpe =>
|
|
||||||
decode(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
val failures = select(Failed)
|
|
||||||
val errors = select(Error)
|
|
||||||
val passed = select(Passed)
|
|
||||||
|
|
||||||
def show(label: String, level: Level.Value, tests: Iterable[String]): Unit =
|
|
||||||
if(!tests.isEmpty)
|
|
||||||
{
|
|
||||||
log.log(level, label)
|
|
||||||
log.log(level, tests.mkString("\t", "\n\t", ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
show("Passed tests:", Level.Debug, passed )
|
|
||||||
show("Failed tests:", Level.Error, failures)
|
|
||||||
show("Error during tests:", Level.Error, errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
results.overall match {
|
|
||||||
case TestResult.Error | TestResult.Failed => throw new TestsFailedException
|
|
||||||
case TestResult.Passed =>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class TestsFailedException extends RuntimeException("Tests unsuccessful") with FeedbackProvidedException
|
final class TestsFailedException extends RuntimeException("Tests unsuccessful") with FeedbackProvidedException
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@ object Defaults extends BuildCommon
|
||||||
},
|
},
|
||||||
testListeners :== Nil,
|
testListeners :== Nil,
|
||||||
testOptions :== Nil,
|
testOptions :== Nil,
|
||||||
|
testResultLogger :== TestResultLogger.Default,
|
||||||
testFilter in testOnly :== (selectedFilter _)
|
testFilter in testOnly :== (selectedFilter _)
|
||||||
))
|
))
|
||||||
lazy val testTasks: Seq[Setting[_]] = testTaskOptions(test) ++ testTaskOptions(testOnly) ++ testTaskOptions(testQuick) ++ testDefaults ++ Seq(
|
lazy val testTasks: Seq[Setting[_]] = testTaskOptions(test) ++ testTaskOptions(testOnly) ++ testTaskOptions(testQuick) ++ testDefaults ++ Seq(
|
||||||
|
|
@ -380,16 +381,15 @@ object Defaults extends BuildCommon
|
||||||
definedTestNames <<= definedTests map ( _.map(_.name).distinct) storeAs definedTestNames triggeredBy compile,
|
definedTestNames <<= definedTests map ( _.map(_.name).distinct) storeAs definedTestNames triggeredBy compile,
|
||||||
testFilter in testQuick <<= testQuickFilter,
|
testFilter in testQuick <<= testQuickFilter,
|
||||||
executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, testExecution in test, fullClasspath in test, javaHome in test, testForkedParallel) flatMap allTestGroupsTask,
|
executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, testExecution in test, fullClasspath in test, javaHome in test, testForkedParallel) flatMap allTestGroupsTask,
|
||||||
|
testResultLogger in (Test, test) :== TestResultLogger.SilentWhenNoTests, // https://github.com/sbt/sbt/issues/1185
|
||||||
test := {
|
test := {
|
||||||
implicit val display = Project.showContextKey(state.value)
|
val trl = (testResultLogger in (Test, test)).value
|
||||||
Tests.showResults(streams.value.log, executeTests.value, noTestsMessage(resolvedScoped.value))
|
val taskName = Project.showContextKey(state.value)(resolvedScoped.value)
|
||||||
|
trl.run(streams.value.log, executeTests.value, taskName)
|
||||||
},
|
},
|
||||||
testOnly <<= inputTests(testOnly),
|
testOnly <<= inputTests(testOnly),
|
||||||
testQuick <<= inputTests(testQuick)
|
testQuick <<= inputTests(testQuick)
|
||||||
)
|
)
|
||||||
private[this] def noTestsMessage(scoped: ScopedKey[_])(implicit display: Show[ScopedKey[_]]): String =
|
|
||||||
"No tests to run for " + display(scoped)
|
|
||||||
|
|
||||||
lazy val TaskGlobal: Scope = ThisScope.copy(task = Global)
|
lazy val TaskGlobal: Scope = ThisScope.copy(task = Global)
|
||||||
lazy val ConfigGlobal: Scope = ThisScope.copy(config = Global)
|
lazy val ConfigGlobal: Scope = ThisScope.copy(config = Global)
|
||||||
def testTaskOptions(key: Scoped): Seq[Setting[_]] = inTask(key)( Seq(
|
def testTaskOptions(key: Scoped): Seq[Setting[_]] = inTask(key)( Seq(
|
||||||
|
|
@ -488,9 +488,9 @@ object Defaults extends BuildCommon
|
||||||
val modifiedOpts = Tests.Filters(filter(selected)) +: Tests.Argument(frameworkOptions : _*) +: config.options
|
val modifiedOpts = Tests.Filters(filter(selected)) +: Tests.Argument(frameworkOptions : _*) +: config.options
|
||||||
val newConfig = config.copy(options = modifiedOpts)
|
val newConfig = config.copy(options = modifiedOpts)
|
||||||
val output = allTestGroupsTask(s, loadedTestFrameworks.value, testLoader.value, testGrouping.value, newConfig, fullClasspath.value, javaHome.value, testForkedParallel.value)
|
val output = allTestGroupsTask(s, loadedTestFrameworks.value, testLoader.value, testGrouping.value, newConfig, fullClasspath.value, javaHome.value, testForkedParallel.value)
|
||||||
val processed =
|
val taskName = display(resolvedScoped.value)
|
||||||
for(out <- output) yield
|
val trl = testResultLogger.value
|
||||||
Tests.showResults(s.log, out, noTestsMessage(resolvedScoped.value))
|
val processed = output.map(out => trl.run(s.log, out, taskName))
|
||||||
Def.value(processed)
|
Def.value(processed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ object Keys
|
||||||
val testForkedParallel = SettingKey[Boolean]("test-forked-parallel", "Whether forked tests should be executed in parallel", CTask)
|
val testForkedParallel = SettingKey[Boolean]("test-forked-parallel", "Whether forked tests should be executed in parallel", CTask)
|
||||||
val testExecution = TaskKey[Tests.Execution]("test-execution", "Settings controlling test execution", DTask)
|
val testExecution = TaskKey[Tests.Execution]("test-execution", "Settings controlling test execution", DTask)
|
||||||
val testFilter = TaskKey[Seq[String] => Seq[String => Boolean]]("test-filter", "Filter controlling whether the test is executed", DTask)
|
val testFilter = TaskKey[Seq[String] => Seq[String => Boolean]]("test-filter", "Filter controlling whether the test is executed", DTask)
|
||||||
|
val testResultLogger = SettingKey[TestResultLogger]("test-result-logger", "Logs results after a test task completes.", DTask)
|
||||||
val testGrouping = TaskKey[Seq[Tests.Group]]("test-grouping", "Collects discovered tests into groups. Whether to fork and the options for forking are configurable on a per-group basis.", BMinusTask)
|
val testGrouping = TaskKey[Seq[Tests.Group]]("test-grouping", "Collects discovered tests into groups. Whether to fork and the options for forking are configurable on a per-group basis.", BMinusTask)
|
||||||
val isModule = AttributeKey[Boolean]("is-module", "True if the target is a module.", DSetting)
|
val isModule = AttributeKey[Boolean]("is-module", "True if the target is a module.", DSetting)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ Changes
|
||||||
0.13.2 to 0.13.5
|
0.13.2 to 0.13.5
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
- The Scala version for sbt and sbt plugins is now 2.10.4. This is a compatible version bump.
|
- The Scala version for sbt and sbt plugins is now 2.10.4. This is a compatible version bump.
|
||||||
|
- Added a new setting ``testResultLogger`` to allow customisation of logging of test results. (gh-1225)
|
||||||
|
- When ``test`` is run and there are no tests available, omit logging output.
|
||||||
|
Especially useful for aggregate modules. ``test-only`` et al unaffected. (gh-1185)
|
||||||
|
|
||||||
0.13.1 to 0.13.2
|
0.13.1 to 0.13.2
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue