diff --git a/main-actions/src/main/scala/sbt/TestResultLogger.scala b/main-actions/src/main/scala/sbt/TestResultLogger.scala index 6d523ea75..5ab02d6c0 100644 --- a/main-actions/src/main/scala/sbt/TestResultLogger.scala +++ b/main-actions/src/main/scala/sbt/TestResultLogger.scala @@ -106,10 +106,9 @@ object TestResultLogger { else run(printFailures) - results.overall match { + results.overall match case TestResult.Error | TestResult.Failed => throw new TestsFailedException - case TestResult.Passed => - } + case TestResult.Empty | TestResult.Passed => () } } @@ -164,11 +163,11 @@ object TestResultLogger { val extra = otherCounts.withFilter(_._2 > 0).map { (label, count) => s", $label $count" } val postfix = base + extra.mkString - results.overall match { + results.overall match + case TestResult.Empty => () 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, _) => { diff --git a/main-actions/src/main/scala/sbt/Tests.scala b/main-actions/src/main/scala/sbt/Tests.scala index ec977e6aa..61dda1875 100644 --- a/main-actions/src/main/scala/sbt/Tests.scala +++ b/main-actions/src/main/scala/sbt/Tests.scala @@ -50,7 +50,7 @@ object Tests { * @param events The result of each test group (suite) executed during this test run. * @param summaries Explicit summaries directly provided by test frameworks. This may be empty, in which case a default summary will be generated. */ - final case class Output( + private[sbt] final case class Output( overall: TestResult, events: Map[String, SuiteResult], summaries: Iterable[Summary] @@ -458,11 +458,10 @@ object Tests { Output(overall(results.map(_._2.result)), results.toMap, Iterable.empty) private def severity(r: TestResult): Int = - r match { - case TestResult.Passed => 0 - case TestResult.Failed => 1 - case TestResult.Error => 2 - } + r match + case TestResult.Passed | TestResult.Empty => 0 + case TestResult.Failed => 1 + case TestResult.Error => 2 def foldTasks(results: Seq[Task[Output]], parallel: Boolean): Task[Output] = if (results.isEmpty) { diff --git a/main-settings/src/main/scala/sbt/SlashSyntax.scala b/main-settings/src/main/scala/sbt/SlashSyntax.scala index f577fd5bc..d7facacec 100644 --- a/main-settings/src/main/scala/sbt/SlashSyntax.scala +++ b/main-settings/src/main/scala/sbt/SlashSyntax.scala @@ -19,7 +19,7 @@ import sbt.Scope.RefThenConfig.{ project, config } * * @example * {{{ - * Test / test := () + * Test / test := TestResult.Empty * console.key / scalacOptions += "-deprecation" * Compile / console / scalacOptions += "-Ywarn-numeric-widen" * }}} diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index a5368b335..09943ddac 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -76,6 +76,7 @@ import sbt.nio.Keys.* import sbt.nio.file.syntax.* import sbt.nio.file.{ FileTreeView, Glob, RecursiveGlob } import sbt.nio.Watch +import sbt.protocol.testing.TestResult import sbt.std.TaskExtra.* import sbt.testing.{ AnnotatedFingerprint, Framework, Runner, SubclassFingerprint } import sbt.util.CacheImplicits.given @@ -1155,7 +1156,10 @@ object Defaults extends BuildCommon { testFull := Def.uncached { val trl = (Test / testFull / testResultLogger).value val taskName = Project.showContextKey(state.value).show(resolvedScoped.value) - try trl.run(streams.value.log, executeTests.value, taskName) + try + val output = executeTests.value + trl.run(streams.value.log, output, taskName) + output.overall finally close(testLoader.value) }, testOnly := { @@ -1287,10 +1291,10 @@ object Defaults extends BuildCommon { } @nowarn - def inputTests(key: InputKey[?]): Initialize[InputTask[Unit]] = + def inputTests(key: InputKey[?]): Initialize[InputTask[TestResult]] = inputTests0.mapReferenced(Def.mapScope((s) => s.rescope(key.key))) - private lazy val inputTests0: Initialize[InputTask[Unit]] = { + private lazy val inputTests0: Initialize[InputTask[TestResult]] = { val parser = loadForParser(definedTestNames)((s, i) => testOnlyParser(s, i getOrElse Nil)) ParserGen(parser).flatMapTask { (selected, frameworkOptions) => val s = streams.value @@ -1318,7 +1322,9 @@ object Defaults extends BuildCommon { val trl = testResultLogger.value (Def .value[Task[Tests.Output]] { output }) - .map { out => trl.run(s.log, out, taskName) } + .map: out => + trl.run(s.log, out, taskName) + out.overall } } diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 02a5c2a56..950cc12ee 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -35,6 +35,7 @@ import sbt.librarymanagement.LibraryManagementCodec.* import sbt.librarymanagement.* import sbt.librarymanagement.ivy.{ Credentials, IvyConfiguration, IvyPaths, UpdateOptions } import sbt.nio.file.Glob +import sbt.protocol.testing.TestResult import sbt.testing.Framework import sbt.util.{ cacheLevel, ActionCacheStore, Digest, Level, Logger, LoggerContext } import xsbti.{ HashedVirtualFileRef, VirtualFile, VirtualFileRef } @@ -357,10 +358,10 @@ object Keys { val definedTestNames = taskKey[Seq[String]]("Provides the set of defined test names.").withRank(BMinusTask) val definedTestDigests = taskKey[Map[String, Digest]]("Provides a unique digest of defined tests.").withRank(DTask) val executeTests = taskKey[Tests.Output]("Executes all tests, producing a report.").withRank(CTask) - val test = inputKey[Unit]("Executes the tests that either failed before, were not run or whose transitive dependencies changed, among those provided as arguments.").withRank(ATask) - val testFull = taskKey[Unit]("Executes all tests.").withRank(APlusTask) - val testOnly = inputKey[Unit]("Executes the tests provided as arguments or all tests if no arguments are provided.").withRank(ATask) - val testQuick = inputKey[Unit]("Alias for test.").withRank(CTask) + val test = inputKey[TestResult]("Executes the tests that either failed before, were not run or whose transitive dependencies changed, among those provided as arguments.").withRank(ATask) + val testFull = taskKey[TestResult]("Executes all tests.").withRank(APlusTask) + val testOnly = inputKey[TestResult]("Executes the tests provided as arguments or all tests if no arguments are provided.").withRank(ATask) + val testQuick = inputKey[TestResult]("Alias for test.").withRank(CTask) @cacheLevel(include = Array.empty) val testOptions = taskKey[Seq[TestOption]]("Options for running tests.").withRank(BPlusTask) private[sbt] val testOptionDigests = taskKey[Seq[Digest]]("Digest for testOptions").withRank(DTask) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index dd606137e..c33d25e6d 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -938,7 +938,9 @@ object BuildServerProtocol { item.classes.toList match { case Nil => Def.task(()) case classes => - (scope / testOnly).toTask(" " + classes.mkString(" ")) + (scope / testOnly) + .toTask(" " + classes.mkString(" ")) + .map(r => ()) } } testTasks.joinWith(ts => TaskExtra.joinTasks(ts).join).result @@ -952,7 +954,7 @@ object BuildServerProtocol { case None => // run allTests in testParams.targets val filter = ScopeFilter.in(testParams.targets.map(workspace.scopes)) - test.toTask("").all(filter).result + test.toTask("").map(r => ()).all(filter).result } Def.task { diff --git a/sbt-app/src/sbt-test/actions/cross-advanced/build.sbt b/sbt-app/src/sbt-test/actions/cross-advanced/build.sbt index e88a82cba..36919cf4a 100644 --- a/sbt-app/src/sbt-test/actions/cross-advanced/build.sbt +++ b/sbt-app/src/sbt-test/actions/cross-advanced/build.sbt @@ -26,7 +26,7 @@ lazy val foo = project // This tests that +testOnly will respect bar's crossScalaVersions and not switch val x = (LocalProject("bar") / scalaVersion).value assert(x == scala212, s"$x == $scala212") - val _ = (Test / testOnly).evaluated + (Test / testOnly).evaluated }, compile2 := Def.uncached { // This tests that +build will ignore bar's crossScalaVersions and use root's like sbt 0.13 diff --git a/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt b/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt index 1508b62a5..07a882033 100644 --- a/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt +++ b/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt @@ -5,7 +5,7 @@ val copyTestResources = inputKey[Unit]("Copy the native libraries to the base di val appendToLibraryPath = taskKey[Unit]("Append the base directory to the java.library.path system property") val dropLibraryPath = taskKey[Unit]("Drop the last path from the java.library.path system property") val wrappedRun = taskKey[Unit]("Run with modified java.library.path") -val wrappedTest = taskKey[Unit]("Test with modified java.library.path") +val wrappedTest = taskKey[TestResult]("Test with modified java.library.path") def wrap[A1](task: InputKey[A1]): Def.Initialize[Task[Unit]] = Def.sequential(appendToLibraryPath, task.toTask(""), dropLibraryPath) @@ -39,5 +39,8 @@ val root = (project in file(".")).settings( () }, wrappedRun := wrap(Runtime / run).value, - wrappedTest := wrap(Test / testOnly).value + wrappedTest := Def.uncached { + wrap(Test / testOnly).value + TestResult.Passed + } ) diff --git a/testing/src/main/contraband-scala/sbt/protocol/testing/TestResult.scala b/testing/src/main/contraband-scala/sbt/protocol/testing/TestResult.scala index 024b495b2..334e85986 100644 --- a/testing/src/main/contraband-scala/sbt/protocol/testing/TestResult.scala +++ b/testing/src/main/contraband-scala/sbt/protocol/testing/TestResult.scala @@ -9,6 +9,7 @@ sealed abstract class TestResult extends Serializable object TestResult { + case object Empty extends TestResult case object Passed extends TestResult case object Failed extends TestResult case object Error extends TestResult diff --git a/testing/src/main/contraband-scala/sbt/protocol/testing/codec/TestResultFormats.scala b/testing/src/main/contraband-scala/sbt/protocol/testing/codec/TestResultFormats.scala index 960fb49d1..e1c2fc270 100644 --- a/testing/src/main/contraband-scala/sbt/protocol/testing/codec/TestResultFormats.scala +++ b/testing/src/main/contraband-scala/sbt/protocol/testing/codec/TestResultFormats.scala @@ -11,6 +11,7 @@ implicit lazy val TestResultFormat: JsonFormat[sbt.protocol.testing.TestResult] __jsOpt match { case Some(__js) => unbuilder.readString(__js) match { + case "Empty" => sbt.protocol.testing.TestResult.Empty case "Passed" => sbt.protocol.testing.TestResult.Passed case "Failed" => sbt.protocol.testing.TestResult.Failed case "Error" => sbt.protocol.testing.TestResult.Error @@ -21,6 +22,7 @@ implicit lazy val TestResultFormat: JsonFormat[sbt.protocol.testing.TestResult] } override def write[J](obj: sbt.protocol.testing.TestResult, builder: Builder[J]): Unit = { val str = obj match { + case sbt.protocol.testing.TestResult.Empty => "Empty" case sbt.protocol.testing.TestResult.Passed => "Passed" case sbt.protocol.testing.TestResult.Failed => "Failed" case sbt.protocol.testing.TestResult.Error => "Error" diff --git a/testing/src/main/contraband/testing.contra b/testing/src/main/contraband/testing.contra index 94cb92425..b6de3bfec 100644 --- a/testing/src/main/contraband/testing.contra +++ b/testing/src/main/contraband/testing.contra @@ -59,6 +59,7 @@ type TestItemDetail { ## Testing result enum TestResult { + Empty Passed Failed Error diff --git a/testing/src/main/scala/sbt/TestReportListener.scala b/testing/src/main/scala/sbt/TestReportListener.scala index 35c6f67d4..2b5a9ee60 100644 --- a/testing/src/main/scala/sbt/TestReportListener.scala +++ b/testing/src/main/scala/sbt/TestReportListener.scala @@ -45,7 +45,7 @@ trait TestsListener extends TestReportListener { /** * Provides the overall `result` of a group of tests (a suite) and test counts for each result type. */ -final class SuiteResult( +private[sbt] final class SuiteResult( val result: TestResult, val passedCount: Int, val failureCount: Int, @@ -99,7 +99,7 @@ final class SuiteResult( } } -object SuiteResult { +private[sbt] object SuiteResult { /** * Computes the overall result and counts for a suite with individual test results in `events`.