Skip to content

Commit

Permalink
Merge pull request #1225 from japgolly/shhh_tests
Browse files Browse the repository at this point in the history
Added setting 'testResultLogger' which allows customisation of test reporting.
  • Loading branch information
eed3si9n committed Apr 9, 2014
2 parents 852d6e2 + 195129a commit 59b834c
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 79 deletions.
159 changes: 159 additions & 0 deletions main/actions/src/main/scala/sbt/TestResultLogger.scala
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
74 changes: 3 additions & 71 deletions main/actions/src/main/scala/sbt/Tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 8 additions & 8 deletions main/src/main/scala/sbt/Defaults.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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)
}
}
Expand Down
1 change: 1 addition & 0 deletions main/src/main/scala/sbt/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions src/sphinx/Community/Changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~~~~
Expand Down

0 comments on commit 59b834c

Please sign in to comment.