diff --git a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala index 5902615a4..698e04def 100644 --- a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala +++ b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala @@ -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)", diff --git a/launcher-package/src/universal/bin/sbt.bat b/launcher-package/src/universal/bin/sbt.bat index 867df3308..b694ca349 100755 --- a/launcher-package/src/universal/bin/sbt.bat +++ b/launcher-package/src/universal/bin/sbt.bat @@ -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 diff --git a/main-actions/src/main/scala/sbt/Tests.scala b/main-actions/src/main/scala/sbt/Tests.scala index 61dda1875..729107d20 100644 --- a/main-actions/src/main/scala/sbt/Tests.scala +++ b/main-actions/src/main/scala/sbt/Tests.scala @@ -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 diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 1c24bda64..ec048292d 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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)) diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index a7eb82ba8..208407585 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -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 { diff --git a/main/src/main/scala/sbt/internal/LibraryManagement.scala b/main/src/main/scala/sbt/internal/LibraryManagement.scala index 23239a51d..3fddb62e8 100644 --- a/main/src/main/scala/sbt/internal/LibraryManagement.scala +++ b/main/src/main/scala/sbt/internal/LibraryManagement.scala @@ -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 diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index 88da9128f..0960d853e 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -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) } diff --git a/main/src/main/scala/sbt/internal/Output.scala b/main/src/main/scala/sbt/internal/Output.scala index 3205b1747..4a0c8c21b 100644 --- a/main/src/main/scala/sbt/internal/Output.scala +++ b/main/src/main/scala/sbt/internal/Output.scala @@ -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[?]] diff --git a/main/src/test/scala/sbt/internal/OutputSpec.scala b/main/src/test/scala/sbt/internal/OutputSpec.scala new file mode 100644 index 000000000..3d9597fc0 --- /dev/null +++ b/main/src/test/scala/sbt/internal/OutputSpec.scala @@ -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") + } +} diff --git a/sbt b/sbt index 308826604..1c290f477 100755 --- a/sbt +++ b/sbt @@ -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 diff --git a/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/build.sbt b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/build.sbt new file mode 100644 index 000000000..c0f23c1d7 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/build.sbt @@ -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 + ) + } + ) diff --git a/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/project/LocalPlugin.scala b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/project/LocalPlugin.scala new file mode 100644 index 000000000..1eee43635 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/project/LocalPlugin.scala @@ -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" + ) +} diff --git a/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/project/build.sbt b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/project/build.sbt new file mode 100644 index 000000000..de5bb733e --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/project/build.sbt @@ -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", + ) + ) \ No newline at end of file diff --git a/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/project/custom/src/main/scala/CustomPlugin.scala b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/project/custom/src/main/scala/CustomPlugin.scala new file mode 100644 index 000000000..c0b64b856 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/project/custom/src/main/scala/CustomPlugin.scala @@ -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 + } + ) +} diff --git a/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/test b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/test new file mode 100644 index 000000000..8a6a426a0 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-local-plugin/test @@ -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 diff --git a/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-missingok/build.sbt b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-missingok/build.sbt new file mode 100644 index 000000000..1b0629c23 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-missingok/build.sbt @@ -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) + }, + ) diff --git a/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-missingok/project/plugins.sbt b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-missingok/project/plugins.sbt new file mode 100644 index 000000000..ddfa827f9 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-missingok/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") diff --git a/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-missingok/test b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-missingok/test new file mode 100644 index 000000000..00ad751bf --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/i8679-update-sbt-classifiers-missingok/test @@ -0,0 +1 @@ +> updateSbtClassifiers diff --git a/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/build.sbt b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/build.sbt new file mode 100644 index 000000000..8cd9690fa --- /dev/null +++ b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/build.sbt @@ -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") + ) diff --git a/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/src/main/scala/custom/CustomReporter.scala b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/src/main/scala/custom/CustomReporter.scala new file mode 100644 index 000000000..d3f20a21b --- /dev/null +++ b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/src/main/scala/custom/CustomReporter.scala @@ -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 _ => + } + } +} diff --git a/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/src/test/scala/com/test/TestSpec.scala b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/src/test/scala/com/test/TestSpec.scala new file mode 100644 index 000000000..05c155b63 --- /dev/null +++ b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/src/test/scala/com/test/TestSpec.scala @@ -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 ` {} +} diff --git a/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/src/test/scala/com/test/TestSpec2.scala b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/src/test/scala/com/test/TestSpec2.scala new file mode 100644 index 000000000..1ff28dffc --- /dev/null +++ b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/src/test/scala/com/test/TestSpec2.scala @@ -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 ` {} +} diff --git a/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/test b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/test new file mode 100644 index 000000000..144967b96 --- /dev/null +++ b/sbt-app/src/sbt-test/tests/i5609-do-not-discover-testonly/test @@ -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 diff --git a/server-test/src/test/scala/testpkg/ClientTest.scala b/server-test/src/test/scala/testpkg/ClientTest.scala index 2b5ae87aa..b20e22d81 100644 --- a/server-test/src/test/scala/testpkg/ClientTest.scala +++ b/server-test/src/test/scala/testpkg/ClientTest.scala @@ -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)