mirror of https://github.com/sbt/sbt.git
Merge pull request #1225 from japgolly/shhh_tests
Added setting 'testResultLogger' which allows customisation of test reporting.
This commit is contained in:
commit
59b834c679
|
|
@ -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)
|
||||
}
|
||||
|
||||
@deprecated("Tests.showResults() has been superseded with TestResultLogger and setting 'testResultLogger'.", "0.13.5")
|
||||
def showResults(log: Logger, results: Output, noTestsMessage: =>String): Unit =
|
||||
{
|
||||
val multipleFrameworks = results.summaries.size > 1
|
||||
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 =>
|
||||
}
|
||||
}
|
||||
TestResultLogger.Default.copy(printNoTests = TestResultLogger.const(_ info noTestsMessage))
|
||||
.run(log, results, "")
|
||||
}
|
||||
|
||||
final class TestsFailedException extends RuntimeException("Tests unsuccessful") with FeedbackProvidedException
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@ object Defaults extends BuildCommon
|
|||
},
|
||||
testListeners :== Nil,
|
||||
testOptions :== Nil,
|
||||
testResultLogger :== TestResultLogger.Default,
|
||||
testFilter in testOnly :== (selectedFilter _)
|
||||
))
|
||||
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,
|
||||
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,
|
||||
testResultLogger in (Test, test) :== TestResultLogger.SilentWhenNoTests, // https://github.com/sbt/sbt/issues/1185
|
||||
test := {
|
||||
implicit val display = Project.showContextKey(state.value)
|
||||
Tests.showResults(streams.value.log, executeTests.value, noTestsMessage(resolvedScoped.value))
|
||||
val trl = (testResultLogger in (Test, test)).value
|
||||
val taskName = Project.showContextKey(state.value)(resolvedScoped.value)
|
||||
trl.run(streams.value.log, executeTests.value, taskName)
|
||||
},
|
||||
testOnly <<= inputTests(testOnly),
|
||||
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 ConfigGlobal: Scope = ThisScope.copy(config = Global)
|
||||
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 newConfig = config.copy(options = modifiedOpts)
|
||||
val output = allTestGroupsTask(s, loadedTestFrameworks.value, testLoader.value, testGrouping.value, newConfig, fullClasspath.value, javaHome.value, testForkedParallel.value)
|
||||
val processed =
|
||||
for(out <- output) yield
|
||||
Tests.showResults(s.log, out, noTestsMessage(resolvedScoped.value))
|
||||
val taskName = display(resolvedScoped.value)
|
||||
val trl = testResultLogger.value
|
||||
val processed = output.map(out => trl.run(s.log, out, taskName))
|
||||
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 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 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 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
|
||||
~~~~~~~~~~~~~~~~
|
||||
- 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
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
Loading…
Reference in New Issue