Merge branch 'develop' into fix/5647-dynamic-all-lint-unused

This commit is contained in:
bitloi 2026-02-14 18:33:29 -05:00 committed by GitHub
commit 5666b2b2d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 455 additions and 23 deletions

View File

@ -4,6 +4,20 @@ package example.test
* RunnerScriptTest is used to test the sbt shell script, for both macOS/Linux and Windows.
*/
object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil:
private val versionPattern = "\\d(\\.\\d+){2}(-\\w+)?"
private def assertScriptVersion(out: List[String]): Unit =
assert(out.mkString(System.lineSeparator()).trim.matches("^" + versionPattern + "$"))
private def assertVersionOutput(out: List[String]): Unit =
val lines =
out.mkString(System.lineSeparator()).linesIterator.map(_.stripPrefix("[0J").trim).toList
assert(
lines.exists(_.matches("^sbt version in this project: " + versionPattern + "\\r?$")) ||
lines.contains("sbtVersion")
)
assert(lines.exists(_.matches("^sbt runner version: " + versionPattern + "\\r?$")))
assert(!lines.exists(_.contains("failed to connect to server")))
testOutput("sbt -no-colors")("compile", "-no-colors", "-v"): (out: List[String]) =>
assert(out.contains[String]("-Dsbt.log.noformat=true"))
@ -117,16 +131,28 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil:
"sbt --script-version should print sbtVersion (sbt 1.x project)",
citestVariant = "citest",
)("--script-version"): (out: List[String]) =>
val expectedVersion = "^" + ExtendedRunnerTest.versionRegEx + "$"
assert(out.mkString(System.lineSeparator()).trim.matches(expectedVersion))
assertScriptVersion(out)
()
testOutput(
"sbt --script-version should print sbtVersion (sbt 2.x project)",
citestVariant = "citest2",
)("--script-version"): (out: List[String]) =>
val expectedVersion = "^" + ExtendedRunnerTest.versionRegEx + "$"
assert(out.mkString(System.lineSeparator()).trim.matches(expectedVersion))
assertScriptVersion(out)
()
testOutput(
"sbt --version should work (sbt 1.x project)",
citestVariant = "citest",
)("--version"): (out: List[String]) =>
assertVersionOutput(out)
()
testOutput(
"sbt --version should work (sbt 2.x project)",
citestVariant = "citest2",
)("--version"): (out: List[String]) =>
assertVersionOutput(out)
()
testOutput("--sbt-cache")("--sbt-cache", "./cachePath"): (out: List[String]) =>
@ -227,6 +253,30 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil:
s"sbtopts options should appear before CLI memory settings. g1Index=$g1Index, xmxCliIndex=$xmxCliIndex"
)
// Test for issue #7333: JVM parameters with spaces in .sbtopts
testOutput(
"sbt with -J--add-modules jdk.incubator.concurrent in .sbtopts (args with spaces)",
sbtOptsFileContents = "-J--add-modules jdk.incubator.concurrent",
windowsSupport = false,
)("-v"): (out: List[String]) =>
assert(out.contains[String]("--add-modules"))
assert(out.contains[String]("jdk.incubator.concurrent"))
// Test for issue #7333: -D with spaces in .jvmopts
testOutput(
"sbt with -Dkey=\"value with spaces\" in .jvmopts",
jvmoptsFileContents = """-Dtest.7333="value with spaces"""",
windowsSupport = false,
)("-v"): (out: List[String]) =>
assert(
out.exists(_.contains("test.7333")),
s"Expected -Dtest.7333= in output, got: ${out.filter(_.contains("test.7333")).mkString(", ")}"
)
assert(
out.exists(_.contains("value with spaces")),
"Expected 'value with spaces' in -D value"
)
// Test for issue #7289: Special characters in .jvmopts should not cause shell expansion
testOutput(
"sbt with special characters in .jvmopts (pipes, wildcards, ampersands)",

View File

@ -617,7 +617,7 @@ if !sbt_args_print_sbt_script_version! equ 1 (
goto :eof
)
if !run_native_client! equ 1 (
if !run_native_client! equ 1 if not defined sbt_args_print_version (
goto :runnative !SBT_ARGS!
goto :eof
)
@ -1122,8 +1122,16 @@ echo.
exit /B 1
:set_sbt_version
rem set project sbtVersion
for /F "usebackq tokens=2" %%G in (`CALL "!_JAVACMD!" -jar "!sbt_jar!" "sbtVersion" 2^>^&1`) do set "sbt_version=%%G"
set "sbt_version="
for /F "usebackq tokens=1,2 delims= " %%a in (`CALL "!_JAVACMD!" -jar "!sbt_jar!" "sbtVersion" 2^>^&1`) do (
if "%%a" == "[info]" (
set "_version_candidate=%%b"
) else (
set "_version_candidate=%%a"
)
echo !_version_candidate!| findstr /R "^[0-9][0-9.]*[-+0-9A-Za-z._]*$" >nul && set "sbt_version=!_version_candidate!"
)
if not defined sbt_version if defined build_props_sbt_version set "sbt_version=!build_props_sbt_version!"
exit /B 0
:error

View File

@ -133,6 +133,9 @@ object Tests {
/** Test execution will be ordered by the position of the matching filter. */
final case class Filters(filterTest: Seq[String => Boolean]) extends TestOption
/** Names explicitly requested (e.g. testOnly com.example.MySuite). Used to set explicitlySpecified on TaskDef. */
final case class ExplicitlyRequestedNames(names: Seq[String]) extends TestOption
/** Defines a TestOption that passes arguments `args` to all test frameworks. */
def Argument(args: String*): Argument = Argument(None, args.toList)
@ -247,10 +250,14 @@ object Tests {
val testFilters = new ListBuffer[String => Boolean]
var orderedFilters = Seq[String => Boolean]()
val excludeTestsSet = new HashSet[String]
var explicitlyRequestedNames = Set.empty[String]
val setup, cleanup = new ListBuffer[ClassLoader => Unit]
val testListeners = new ListBuffer[TestReportListener]
val undefinedFrameworks = new ListBuffer[String]
def isExplicitFqn(s: String): Boolean =
!s.contains('*') && !s.contains('?') && !s.contains("...")
for (option <- config.options) {
option match {
case Filter(include) => testFilters += include; ()
@ -258,6 +265,9 @@ object Tests {
if (orderedFilters.nonEmpty) sys.error("Cannot define multiple ordered test filters.")
else orderedFilters = includes
()
case ExplicitlyRequestedNames(names) =>
explicitlyRequestedNames = names.filter(isExplicitFqn).toSet
()
case Exclude(exclude) => excludeTestsSet ++= exclude; ()
case Listeners(listeners) => testListeners ++= listeners; ()
case Setup(setupFunction, _) => setup += setupFunction; ()
@ -281,8 +291,18 @@ object Tests {
if (orderedFilters.isEmpty) filtered0
else orderedFilters.flatMap(f => filtered0.filter(d => f(d.name))).toList.distinct
val uniqueTests = distinctBy(tests)(_.name)
// Per TaskDef: explicitlySpecified=true only when user supplied a complete FQN (e.g. testOnly com.example.MySuite),
// not for patterns (testOnly *Spec) or plain "test". So only mark when test.name is in explicitlyRequestedNames.
val testsToUse = uniqueTests.map(t =>
new TestDefinition(
t.name,
t.fingerprint,
explicitlySpecified = explicitlyRequestedNames.contains(t.name),
t.selectors
)
)
new ProcessedOptions(
uniqueTests.toVector,
testsToUse.toVector,
setup.toVector,
cleanup.toVector,
testListeners.toVector
@ -555,7 +575,6 @@ object Tests {
c.topLevel
case _ => false
})
// TODO: To pass in correct explicitlySpecified and selectors
val tests =
for {
(df, di) <- discovered

View File

@ -1354,7 +1354,8 @@ object Defaults extends BuildCommon {
val st = state.value
given display: Show[ScopedKey[?]] = Project.showContextKey(st)
val modifiedOpts =
Tests.Filters(filter(selected)) +: Tests.Argument(frameworkOptions*) +: config.options
Tests.ExplicitlyRequestedNames(selected) +: Tests.Filters(filter(selected)) +:
Tests.Argument(frameworkOptions*) +: config.options
val newConfig = config.copy(options = modifiedOpts)
val output = allTestGroupsTask(
s,
@ -3633,6 +3634,7 @@ object Classpaths {
.withArtifactFilter(
updateConfig0.artifactFilter.map(af => af.withInverted(!af.inverted))
)
.withMissingOk(true)
val app = appConfiguration.value
val srcTypes = sourceArtifactTypes.value
val docTypes = docArtifactTypes.value
@ -3718,11 +3720,14 @@ object Classpaths {
val ref = thisProjectRef.value
val unit = loadedBuild.value.units(ref.build).unit
val converter = unit.converter
val pluginClasspath = unit.plugins.pluginData.dependencyClasspath.toVector
val pluginData = unit.plugins.pluginData
val pluginInternalCp = pluginData.internalDependencyClasspath.toVector
val pluginClasspath = pluginData.dependencyClasspath.toVector
val cp = pluginClasspath.diff(pluginInternalCp)
// Exclude directories: an approximation to whether they've been published
// Note: it might be a redundant legacy from sbt 0.13/1.x times where the classpath contained directories
// but it's left just in case
val pluginJars = pluginClasspath.filter: x =>
val pluginJars = cp.filter: x =>
!Files.isDirectory(converter.toPath(x.data))
val pluginIDs: Vector[ModuleID] = pluginJars.flatMap(_.get(moduleIDStr).map: str =>
moduleIdJsonKeyFormat.read(str))

View File

@ -142,6 +142,10 @@ object EvaluateTaskConfig {
) extends EvaluateTaskConfig
}
/**
* @param internalDependencyClasspath internal classpath entries from the metabuild that are used to exclude
* them when resolving/retrieving classifiers for sbt.
*/
final case class PluginData(
dependencyClasspath: Def.Classpath,
definitionClasspath: Def.Classpath,
@ -155,13 +159,28 @@ final case class PluginData(
managedSources: Seq[File],
buildTarget: Option[BuildTargetIdentifier],
converter: FileConverter,
internalDependencyClasspath: Def.Classpath,
) {
val classpath: Def.Classpath = definitionClasspath ++ dependencyClasspath
}
object PluginData {
private[sbt] def apply(dependencyClasspath: Def.Classpath, converter: FileConverter): PluginData =
PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil, Nil, None, converter)
PluginData(
dependencyClasspath,
Nil,
None,
None,
Nil,
Nil,
Nil,
Nil,
Nil,
Nil,
None,
converter,
Nil
)
}
object EvaluateTask {

View File

@ -172,6 +172,11 @@ private[sbt] object LibraryManagement {
.apply(updateInputs)
if isCached then markAsCached(report) else report
catch
case r: ResolveException
if r.failed.exists(isMissingScalaLib) &&
module.scalaModuleInfo.exists(_.scalaBinaryVersion == "2.13") =>
informSandwich()
throw r
case t: (NullPointerException | OutOfMemoryError) =>
val resolvedAgain = resolve
val culprit = t.getClass.getSimpleName
@ -185,6 +190,26 @@ private[sbt] object LibraryManagement {
Tracked.inputChanged(cacheStoreFactory.make("inputs"))(doCachedResolve)
}
def informSandwich(): Unit =
log.warn("[sbt-8728] Smorrebrod - the end of Scala 2.13-3.x sandwich")
log.warn("")
log.warn("Scala 3.8+ cannot be used in a Scala 2.13 subproject.")
log.warn(
"Dependency resolution failed because scala-reflect or -compiler 3.x does not exist."
)
log.warn(
"This happens when a Scala 2.13 subproject depends on Scala 3.8+ directly or transitively."
)
log.warn("To fix this, either")
log.warn(" - Keep Scala 3 subproject or transitive dependency to 3.7 or below, or")
log.warn(" - Migrate the Scala 2.13 subproject to Scala 3.x")
log.warn("See https://github.com/sbt/sbt/discussions/8728")
def isMissingScalaLib(m: ModuleID): Boolean =
m.organization == "org.scala-lang" &&
(m.name == "scala-compiler" || m.name == "scala-reflect") &&
(m.revision.startsWith("3."))
// Get the handler to use and feed it in the inputs
// This is lm-engine specific input hashed into Long
val extraInputHash = module.extraInputHash

View File

@ -1381,6 +1381,7 @@ private[sbt] object Load {
isMetaBuild :== true,
pluginData := Def.uncached {
val prod = (Configurations.Runtime / exportedProducts).value
val internalCp = (Configurations.Runtime / internalDependencyClasspath).value
val cp = (Configurations.Runtime / fullClasspath).value
val opts = (Configurations.Compile / scalacOptions).value
val javaOpts = (Configurations.Compile / javacOptions).value
@ -1403,6 +1404,7 @@ private[sbt] object Load {
managedSrcs,
Some(buildTarget),
converter,
internalCp,
)
},
onLoadMessage := ("loading project definition from " + baseDirectory.value)
@ -1670,6 +1672,7 @@ final case class LoadBuildConfiguration(
Nil,
None,
converter,
Nil
)
case None => PluginData(globalPluginClasspath, converter)
}

View File

@ -19,6 +19,7 @@ import Aggregation.{ KeyValue, Values }
import Types.idFun
import Highlight.{ bold, showMatches }
import annotation.tailrec
import sbt.internal.util.EscHelpers
import sbt.io.IO
@ -43,7 +44,12 @@ object Output {
printLines: Seq[String] => Unit
)(using display: Show[ScopedKey[?]]): Unit = {
val pattern = Pattern.compile(patternString)
val lines = flatLines(lastLines(keys, streams))(_ flatMap showMatches(pattern))
val lines = flatLines(lastLines(keys, streams)) { rawLines =>
rawLines.flatMap { line =>
val stripped = EscHelpers.stripColorsAndMoves(line)
showMatches(pattern)(stripped)
}
}
printLines(lines)
}
@ -55,8 +61,13 @@ object Output {
): Unit =
printLines(grep(tailLines(file, tailDelim), patternString))
def grep(lines: Seq[String], patternString: String): Seq[String] =
lines.flatMap(showMatches(Pattern.compile(patternString)))
def grep(lines: Seq[String], patternString: String): Seq[String] = {
val pattern = Pattern.compile(patternString)
lines.flatMap { line =>
val stripped = EscHelpers.stripColorsAndMoves(line)
showMatches(pattern)(stripped)
}
}
def flatLines(outputs: Values[Seq[String]])(f: Seq[String] => Seq[String])(using
display: Show[ScopedKey[?]]

View File

@ -0,0 +1,35 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import scala.Console.{ RED, RESET }
import verify.BasicTestSuite
import sbt.internal.Output.grep
object OutputSpec extends BasicTestSuite {
test(
"grep should match pattern against visible text when lines contain ANSI escape sequences (#4840)"
) {
// Line with ANSI color around "error" - user searching for "error" should find it (strip before match)
val lineWithAnsi = s"${RED}error${RESET}: something failed"
val lines = Seq("info: ok", lineWithAnsi, "warn: deprecated")
val result = grep(lines, "error")
assert(result.size == 1, s"expected 1 match, got ${result.size}: $result")
// Pattern matched the visible "error" (ANSI was stripped before matching); result may have highlight from showMatches
assert(result.head.contains("error"), s"result should contain 'error': ${result.head}")
}
test("grep should not match when pattern appears only inside ANSI sequence") {
// Line where "error" is not in the visible text (only in escape code - unrealistic but ensures we strip first)
val lines = Seq("info: ok", "something failed")
val result = grep(lines, "error")
assert(result.isEmpty, s"expected no match, got: $result")
}
}

67
sbt
View File

@ -766,6 +766,60 @@ process_args () {
}
}
# Parse a line into words respecting double and single quotes.
# Outputs one word per line. Used for .sbtopts and .jvmopts to handle args with spaces (#7333).
parseLineIntoWords() {
local line="$1"
local word=""
local i=0
local len=${#line}
local in_dq=0 in_sq=0
while (( i < len )); do
local c="${line:$i:1}"
if (( in_dq )); then
word+="$c"
[[ "$c" == '"' ]] && in_dq=0
elif (( in_sq )); then
word+="$c"
[[ "$c" == "'" ]] && in_sq=0
else
case "$c" in
'"') in_dq=1; word+="$c" ;;
"'") in_sq=1; word+="$c" ;;
' '|$'\t')
[[ -n "$word" ]] && printf '%s\n' "$word"
word=""
;;
*) word+="$c" ;;
esac
fi
((i++))
done
[[ -n "$word" ]] && printf '%s\n' "$word"
}
# Output config file tokens one per line. For -J lines, each token is prefixed with -J.
# No eval; caller appends via: while IFS= read -r t; do [[ -n "$t" ]] && arr+=("$t"); done < <(outputConfigFileTokens "$file")
# Fixes #7333; Bash 3.x compatible.
outputConfigFileTokens() {
local file="$1"
[[ ! -f "$file" ]] && return
while IFS= read -r line || [[ -n "$line" ]]; do
line=$(printf '%s' "$line" | sed $'/^\#/d;s/\r$//')
[[ -z "$line" ]] && continue
if [[ "$line" == -J* ]]; then
local rest="${line#-J}"
while IFS= read -r token; do
[[ -n "$token" ]] && printf '%s\n' "-J$token"
done < <(parseLineIntoWords "$rest")
else
while IFS= read -r token; do
[[ -n "$token" ]] && printf '%s\n' "$token"
done < <(parseLineIntoWords "$line")
fi
done < <(cat "$file")
}
loadConfigFile() {
# Make sure the last line is read even if it doesn't have a terminating \n
# Output lines literally without shell expansion to handle special characters safely
@ -861,14 +915,14 @@ sbt_file_opts=()
# Pull in the machine-wide settings configuration.
if [[ -f "$machine_sbt_opts_file" ]]; then
sbt_file_opts+=($(loadConfigFile "$machine_sbt_opts_file"))
while IFS= read -r t; do [[ -n "$t" ]] && sbt_file_opts+=("$t"); done < <(outputConfigFileTokens "$machine_sbt_opts_file")
else
# Otherwise pull in the default settings configuration.
[[ -f "$dist_sbt_opts_file" ]] && sbt_file_opts+=($(loadConfigFile "$dist_sbt_opts_file"))
[[ -f "$dist_sbt_opts_file" ]] && while IFS= read -r t; do [[ -n "$t" ]] && sbt_file_opts+=("$t"); done < <(outputConfigFileTokens "$dist_sbt_opts_file")
fi
# Pull in the project-level config file, if it exists (highest priority, overrides machine/dist).
[[ -f "$sbt_opts_file" ]] && sbt_file_opts+=($(loadConfigFile "$sbt_opts_file"))
[[ -f "$sbt_opts_file" ]] && while IFS= read -r t; do [[ -n "$t" ]] && sbt_file_opts+=("$t"); done < <(outputConfigFileTokens "$sbt_opts_file")
# Prepend sbtopts so command line args appear last and win for duplicate properties.
if (( ${#sbt_file_opts[@]} > 0 )); then
@ -876,14 +930,15 @@ if (( ${#sbt_file_opts[@]} > 0 )); then
fi
# Pull in the project-level java config, if it exists.
[[ -f ".jvmopts" ]] && export JAVA_OPTS="$JAVA_OPTS $(loadConfigFile .jvmopts)"
jvmopts_args=()
[[ -f ".jvmopts" ]] && while IFS= read -r t; do [[ -n "$t" ]] && jvmopts_args+=("$t"); done < <(outputConfigFileTokens ".jvmopts")
# Pull in default JAVA_OPTS
[[ -z "${JAVA_OPTS// }" ]] && export JAVA_OPTS="$default_java_opts"
[[ -f "$build_props_file" ]] && loadPropFile "$build_props_file"
java_args=($JAVA_OPTS)
java_args=($JAVA_OPTS "${jvmopts_args[@]}")
sbt_options0=(${SBT_OPTS:-$default_sbt_opts})
java_tool_options=($JAVA_TOOL_OPTIONS)
jdk_java_options=($JDK_JAVA_OPTIONS)
@ -909,7 +964,7 @@ if [[ $print_sbt_script_version ]]; then
exit 0
fi
if [[ "$(isRunNativeClient)" == "true" ]]; then
if [[ "$(isRunNativeClient)" == "true" ]] && [[ -z "$print_version" ]]; then
set -- "${residual_args[@]}"
argumentCount=$#
runNativeClient

View File

@ -0,0 +1,39 @@
lazy val root = (project in file("."))
.settings(
// Verify that local plugins work
TaskKey[Unit]("checkLocalPlugins") := Def.uncached {
val localResult = localPluginCheck.value
val customResult = customPluginCheck.value
assert(localResult == "local-plugin-active", s"Expected local plugin to be active, got: $localResult")
assert(customResult == "custom plugin", s"Expected custom plugin to be active, got: $customResult")
},
// Verify that the dependencies in updateSbtClassifiers / classifiersModule do not include the local plugins but do include
// other declared dependencies
TaskKey[Unit]("checkClassifiersModule") := Def.uncached {
val mod = (updateSbtClassifiers / classifiersModule).value
val deps = mod.dependencies
val actual = deps.map(m => s"${m.organization}:${m.name}").sorted.toSet
val expected = Set(
"org.scala-sbt:sbt",
"junit:junit",
"com.eed3si9n:sbt-buildinfo_sbt2_3",
"org.hamcrest:hamcrest-core",
"com.eed3si9n.manifesto:manifesto_3",
"org.scala-lang:scala3-library_3",
"org.scala-lang:scala-library",
"org.typelevel:cats-core_3",
"org.typelevel:cats-kernel_3"
)
assert(
actual == expected,
s"""
|ClassifiersModule dependencies mismatch.
|Expected: ${expected.mkString(", ")}
|Actual: ${actual.mkString(", ")}
""".stripMargin
)
}
)

View File

@ -0,0 +1,17 @@
import sbt.*
import sbt.Keys.*
object LocalPlugin extends AutoPlugin {
override def requires: Plugins = plugins.JvmPlugin
override def trigger: PluginTrigger = allRequirements
object autoImport {
val localPluginCheck = taskKey[String]("A task provided by the local plugin")
}
import autoImport.*
override lazy val projectSettings: Seq[Setting[?]] = Seq(
localPluginCheck := "local-plugin-active"
)
}

View File

@ -0,0 +1,16 @@
lazy val meta = (project in file("."))
.settings(
// Just to make the test more comprehensive and check whether the additional libraries/plugins
// are present in updateSbtClassifiers/classifiersModule.
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1"),
libraryDependencies += "junit" % "junit" % "4.13.2"
)
.dependsOn(customPlugin)
lazy val customPlugin = (project in file("custom"))
.enablePlugins(SbtPlugin)
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.13.0",
)
)

View File

@ -0,0 +1,20 @@
import sbt.*
import sbt.Keys.*
object CustomPlugin extends AutoPlugin {
override def requires: Plugins = plugins.JvmPlugin
override def trigger: PluginTrigger = allRequirements
object autoImport {
val customPluginCheck = taskKey[String]("A task provided by the custom plugin")
}
import autoImport.*
override lazy val projectSettings: Seq[Setting[?]] = Seq(
customPluginCheck := {
import cats.implicits.*
List("custom", " ", "plugin").combineAll
}
)
}

View File

@ -0,0 +1,8 @@
# Verify the local plugins are loaded and active
> checkLocalPlugins
# Verify the local plugins are not included in the classifiersModule dependencies
> checkClassifiersModule
# Also verify updateSbtClassifiers itself succeeds
> updateSbtClassifiers

View File

@ -0,0 +1,11 @@
lazy val root = (project in file("."))
.settings(
// Inject a non-existent module into the classifiers module dependencies
// to simulate a scenario where classifier artifacts can't be downloaded.
// With missingOk=true, updateSbtClassifiers should still succeed.
updateSbtClassifiers / classifiersModule := {
val mod = (updateSbtClassifiers / classifiersModule).value
val fakeModule = "com.example.nonexistent" % "fake-library" % "0.0.1"
mod.withDependencies(mod.dependencies :+ fakeModule)
},
)

View File

@ -0,0 +1 @@
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1")

View File

@ -0,0 +1 @@
> updateSbtClassifiers

View File

@ -0,0 +1,9 @@
val scalatest = "org.scalatest" %% "scalatest" % "3.0.5"
ThisBuild / scalaVersion := "2.12.21"
lazy val root = (project in file("."))
.settings(
libraryDependencies += scalatest,
Test / testOptions += Tests.Argument("-C", "custom.CustomReporter")
)

View File

@ -0,0 +1,30 @@
package custom
import java.io._
import org.scalatest._
import events._
class CustomReporter extends Reporter {
private def writeFile(filePath: String, content: String): Unit = {
val file = new File(filePath)
val writer =
if (!file.exists)
new FileWriter(new File(filePath))
else
new FileWriter(new File(filePath + "-2"))
writer.write(content)
writer.flush()
writer.close()
}
def apply(event: Event): Unit = {
event match {
case SuiteStarting(_, suiteName, _, _, _, _, _, _, _, _) => writeFile("target/SuiteStarting-" + suiteName, suiteName)
case SuiteCompleted(_, suiteName, _, _, _, _, _, _, _, _, _) => writeFile("target/SuiteCompleted-" + suiteName, suiteName)
case TestStarting(_, _, _, _, testName, _, _, _, _, _, _, _) => writeFile("target/TestStarting-" + testName, testName)
case TestSucceeded(_, _, _, _, testName, _, _, _, _, _, _, _, _, _) => writeFile("target/TestSucceeded-" + testName, testName)
case _ =>
}
}
}

View File

@ -0,0 +1,12 @@
package com.test
import org.scalatest.Spec
class TestSpec extends Spec {
def `TestSpec-test-1 ` {}
def `TestSpec-test-2 ` {}
def `TestSpec-test-3 ` {}
}

View File

@ -0,0 +1,13 @@
package com.test
import org.scalatest._
@DoNotDiscover
class TestSpec2 extends Spec {
def `TestSpec2-test-1 ` {}
def `TestSpec2-test-2 ` {}
def `TestSpec2-test-3 ` {}
}

View File

@ -0,0 +1,25 @@
# #5609: When explicitly requested via testOnly, @DoNotDiscover suite should run.
# First: full test run must exclude @DoNotDiscover (TestSpec2).
# Second: testOnly with explicit FQN must run TestSpec2.
> clean
> testFull
$ exists target/SuiteStarting-TestSpec
$ exists target/SuiteCompleted-TestSpec
$ absent target/SuiteStarting-TestSpec2
$ absent target/SuiteCompleted-TestSpec2
> clean
> testOnly com.test.TestSpec2
$ exists target/SuiteStarting-TestSpec2
$ exists target/SuiteCompleted-TestSpec2
$ delete target/SuiteStarting-TestSpec
$ delete target/SuiteCompleted-TestSpec
$ delete target/SuiteStarting-TestSpec2
$ delete target/SuiteCompleted-TestSpec2
> testOnly com.test...
$ exists target/SuiteStarting-TestSpec
$ exists target/SuiteCompleted-TestSpec
$ absent target/SuiteStarting-TestSpec2
$ absent target/SuiteCompleted-TestSpec2

View File

@ -117,7 +117,7 @@ class ClientTest extends AbstractServerTest {
assert(client("willFail;willSucceed") == 1)
}
test("three commands") {
assert(client("compile;clean;willSucceed") == 0)
assert(client("compile;willSucceed;willSucceed") == 0)
}
test("three commands with middle failure") {
assert(client("compile;willFail;willSucceed") == 1)