Skip to content

Commit

Permalink
Include test scope in the REPL when the --test flag is passed
Browse files Browse the repository at this point in the history
  • Loading branch information
Gedochao committed Jun 18, 2024
1 parent acfcc17 commit 2c1a420
Show file tree
Hide file tree
Showing 14 changed files with 259 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers {
None,
logger,
crossBuilds = cross,
buildTests = options.test,
buildTests = options.scope.test,
partial = None,
actionableDiagnostics = actionableDiagnostics,
postAction = () => WatchUtil.printWatchMessage()
Expand All @@ -128,7 +128,7 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers {
None,
logger,
crossBuilds = cross,
buildTests = options.test,
buildTests = options.scope.test,
partial = None,
actionableDiagnostics = actionableDiagnostics
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@ package scala.cli.commands.compile
import caseapp.*
import caseapp.core.help.Help

import scala.cli.commands.shared.{
CrossOptions,
HasSharedOptions,
HelpGroup,
HelpMessages,
SharedOptions,
SharedWatchOptions
}
import scala.cli.commands.shared._
import scala.cli.commands.tags

@HelpMessage(CompileOptions.helpMessage, "", CompileOptions.detailedHelpMessage)
Expand All @@ -31,11 +24,8 @@ final case class CompileOptions(
@Tag(tags.inShortHelp)
printClassPath: Boolean = false,

@Group(HelpGroup.Compilation.toString)
@HelpMessage("Compile test scope")
@Tag(tags.should)
@Tag(tags.inShortHelp)
test: Boolean = false
@Recurse
scope: ScopeOptions = ScopeOptions()
) extends HasSharedOptions
// format: on

Expand Down
148 changes: 81 additions & 67 deletions modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import java.util.zip.ZipFile

import scala.build.EitherCps.{either, value}
import scala.build.*
import scala.build.errors.{BuildException, CantDownloadAmmoniteError, FetchingDependenciesError}
import scala.build.errors.{
BuildException,
CantDownloadAmmoniteError,
FetchingDependenciesError,
MultipleScalaVersionsError
}
import scala.build.input.Inputs
import scala.build.internal.{Constants, Runner}
import scala.build.options.{BuildOptions, JavaOpt, MaybeScalaVersion, Scope}
Expand All @@ -24,6 +29,7 @@ import scala.cli.commands.run.Run.{
}
import scala.cli.commands.run.RunMode
import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions}
import scala.cli.commands.util.BuildCommandHelpers
import scala.cli.commands.{ScalaCommand, WatchUtil}
import scala.cli.config.{ConfigDb, Keys}
import scala.cli.packaging.Library
Expand All @@ -33,7 +39,7 @@ import scala.cli.{CurrentParams, ScalaCli}
import scala.jdk.CollectionConverters.*
import scala.util.Properties

object Repl extends ScalaCommand[ReplOptions] {
object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers {
override def group: String = HelpCommandGroup.Main.toString
override def scalaSpecificationLevel = SpecificationLevel.MUST
override def helpFormat: HelpFormat = super.helpFormat
Expand Down Expand Up @@ -117,36 +123,25 @@ object Repl extends ScalaCommand[ReplOptions] {

val directories = Directories.directories

def buildFailed(allowExit: Boolean): Unit = {
System.err.println("Compilation failed")
if (allowExit)
sys.exit(1)
}
def buildCancelled(allowExit: Boolean): Unit = {
System.err.println("Build cancelled")
if (allowExit)
sys.exit(1)
}

def doRunRepl(
buildOptions: BuildOptions,
artifacts: Artifacts,
mainJarOrClassDir: Option[os.Path],
allArtifacts: Seq[Artifacts],
mainJarsOrClassDirs: Seq[os.Path],
allowExit: Boolean,
runMode: RunMode.HasRepl,
buildOpt: Option[Build.Successful]
successfulBuilds: Seq[Build.Successful]
): Unit = {
val res = runRepl(
buildOptions,
programArgs,
artifacts,
mainJarOrClassDir,
directories,
logger,
options = buildOptions,
programArgs = programArgs,
allArtifacts = allArtifacts,
mainJarsOrClassDirs = mainJarsOrClassDirs,
directories = directories,
logger = logger,
allowExit = allowExit,
options.sharedRepl.replDryRun,
runMode,
buildOpt
dryRun = options.sharedRepl.replDryRun,
runMode = runMode,
successfulBuilds = successfulBuilds
)
res match {
case Left(ex) =>
Expand All @@ -156,19 +151,23 @@ object Repl extends ScalaCommand[ReplOptions] {
}
}
def doRunReplFromBuild(
build: Build.Successful,
builds: Seq[Build.Successful],
allowExit: Boolean,
runMode: RunMode.HasRepl,
asJar: Boolean
): Unit =
): Unit = {
doRunRepl(
build.options,
build.artifacts,
Some(if (asJar) Library.libraryJar(build) else build.output),
allowExit,
runMode,
Some(build)
// build options should be the same for both scopes
// combining them may cause for ammonite args to be duplicated, so we're using the main scope's opts
buildOptions = builds.head.options,
allArtifacts = builds.map(_.artifacts),
mainJarsOrClassDirs =
if (asJar) builds.map(Library.libraryJar(_)) else builds.map(_.output),
allowExit = allowExit,
runMode = runMode,
successfulBuilds = builds
)
}

val cross = options.sharedRepl.compileCross.cross.getOrElse(false)
val configDb = ConfigDbUtils.configDb.orExit(logger)
Expand All @@ -178,18 +177,22 @@ object Repl extends ScalaCommand[ReplOptions] {
)

if (inputs.isEmpty) {
val artifacts = initialBuildOptions.artifacts(logger, Scope.Main).orExit(logger)
val allArtifacts =
Seq(initialBuildOptions.artifacts(logger, Scope.Main).orExit(logger)) ++
(if options.sharedRepl.scope.test
then Seq(initialBuildOptions.artifacts(logger, Scope.Test).orExit(logger))
else Nil)
// synchronizing, so that multiple presses to enter (handled by WatchUtil.waitForCtrlC)
// don't try to run repls in parallel
val lock = new Object
def runThing() = lock.synchronized {
doRunRepl(
initialBuildOptions,
artifacts,
None,
buildOptions = initialBuildOptions,
allArtifacts = allArtifacts,
mainJarsOrClassDirs = Seq.empty,
allowExit = !options.sharedRepl.watch.watchMode,
runMode = runMode(options),
buildOpt = None
successfulBuilds = Seq.empty
)
}
runThing()
Expand All @@ -207,22 +210,20 @@ object Repl extends ScalaCommand[ReplOptions] {
None,
logger,
crossBuilds = cross,
buildTests = false,
buildTests = options.sharedRepl.scope.test,
partial = None,
actionableDiagnostics = actionableDiagnostics,
postAction = () => WatchUtil.printWatchMessage()
) { res =>
for (builds <- res.orReport(logger))
builds.main match {
case s: Build.Successful =>
postBuild(builds, allowExit = false) {
successfulBuilds =>
doRunReplFromBuild(
s,
successfulBuilds,
allowExit = false,
runMode = runMode(options),
asJar = options.shared.asJar
)
case _: Build.Failed => buildFailed(allowExit = false)
case _: Build.Cancelled => buildCancelled(allowExit = false)
}
}
try WatchUtil.waitForCtrlC(() => watcher.schedule())
Expand All @@ -237,25 +238,35 @@ object Repl extends ScalaCommand[ReplOptions] {
None,
logger,
crossBuilds = cross,
buildTests = false,
buildTests = options.sharedRepl.scope.test,
partial = None,
actionableDiagnostics = actionableDiagnostics
)
.orExit(logger)
builds.main match {
case s: Build.Successful =>
postBuild(builds, allowExit = false) {
successfulBuilds =>
doRunReplFromBuild(
s,
successfulBuilds,
allowExit = true,
runMode = runMode(options),
asJar = options.shared.asJar
)
case _: Build.Failed => buildFailed(allowExit = true)
case _: Build.Cancelled => buildCancelled(allowExit = true)
}
}
}

def postBuild(builds: Builds, allowExit: Boolean)(f: Seq[Build.Successful] => Unit): Unit = {
if builds.anyBuildFailed then {
System.err.println("Compilation failed")
if allowExit then sys.exit(1)
}
else if builds.anyBuildCancelled then {
System.err.println("Build cancelled")
if allowExit then sys.exit(1)
}
else f((Seq(builds.main) ++ builds.get(Scope.Test).toSeq).map(_.asInstanceOf[Build.Successful]))
}

private def maybeAdaptForWindows(args: Seq[String]): Seq[String] =
if (Properties.isWin)
args.map { a =>
Expand All @@ -268,24 +279,28 @@ object Repl extends ScalaCommand[ReplOptions] {
private def runRepl(
options: BuildOptions,
programArgs: Seq[String],
artifacts: Artifacts,
mainJarOrClassDir: Option[os.Path],
allArtifacts: Seq[Artifacts],
mainJarsOrClassDirs: Seq[os.Path],
directories: scala.build.Directories,
logger: Logger,
allowExit: Boolean,
dryRun: Boolean,
runMode: RunMode.HasRepl,
buildOpt: Option[Build.Successful]
successfulBuilds: Seq[Build.Successful]
): Either[BuildException, Unit] = either {

val setupPython = options.notForBloopOptions.python.getOrElse(false)

val cache = options.internal.cache.getOrElse(FileCache())
val shouldUseAmmonite = options.notForBloopOptions.replOptions.useAmmonite

val scalaParams = artifacts.scalaOpt match {
case Some(artifacts) => artifacts.params
case None => ScalaParameters(Constants.defaultScalaVersion)
val scalaParams: ScalaParameters = value {
val distinctScalaArtifacts = allArtifacts.flatMap(_.scalaOpt).distinctBy(_.params)
if distinctScalaArtifacts.isEmpty then
Right(ScalaParameters(Constants.defaultScalaVersion))
else if distinctScalaArtifacts.length == 1 then
Right(distinctScalaArtifacts.head.params)
else Left(MultipleScalaVersionsError(distinctScalaArtifacts.map(_.params.scalaVersion)))
}

val (scalapyJavaOpts, scalapyExtraEnv) =
Expand All @@ -302,7 +317,7 @@ object Repl extends ScalaCommand[ReplOptions] {
// Putting current dir in PYTHONPATH, see
// https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174
// for context.
val dirs = buildOpt.map(_.inputs.workspace).toSeq ++ Seq(os.pwd)
val dirs = successfulBuilds.map(_.inputs.workspace) ++ Seq(os.pwd)
(props0, pythonPathEnv(dirs: _*))
}
else
Expand Down Expand Up @@ -338,15 +353,14 @@ object Repl extends ScalaCommand[ReplOptions] {

// TODO Allow to disable printing the welcome banner and the "Loading..." message in Ammonite.

val rootClasses = mainJarOrClassDir match {
case None => Nil
case Some(dir) if os.isDir(dir) =>
val rootClasses = mainJarsOrClassDirs.flatMap {
case dir if os.isDir(dir) =>
os.list(dir)
.filter(_.last.endsWith(".class"))
.filter(os.isFile(_)) // just in case
.map(_.last.stripSuffix(".class"))
.sorted
case Some(jar) =>
case jar =>
var zf: ZipFile = null
try {
zf = new ZipFile(jar.toIO)
Expand Down Expand Up @@ -396,7 +410,7 @@ object Repl extends ScalaCommand[ReplOptions] {
replArtifacts.replJavaOpts ++
options.javaOptions.javaOpts.toSeq.map(_.value.value) ++
extraProps.toVector.sorted.map { case (k, v) => s"-D$k=$v" },
classPath = mainJarOrClassDir.toSeq ++ replArtifacts.replClassPath,
classPath = mainJarsOrClassDirs ++ replArtifacts.replClassPath,
mainClass = replArtifacts.replMainClass,
args = maybeAdaptForWindows(depClassPathArgs ++ replArgs),
logger = logger,
Expand All @@ -411,8 +425,8 @@ object Repl extends ScalaCommand[ReplOptions] {
value {
ReplArtifacts.default(
scalaParams,
artifacts.userDependencies,
artifacts.extraClassPath,
allArtifacts.flatMap(_.userDependencies).distinct,
allArtifacts.flatMap(_.extraClassPath).distinct,
logger,
cache,
value(options.finalRepositories),
Expand All @@ -427,9 +441,9 @@ object Repl extends ScalaCommand[ReplOptions] {
ReplArtifacts.ammonite(
scalaParams,
options.notForBloopOptions.replOptions.ammoniteVersion(scalaParams.scalaVersion, logger),
artifacts.userDependencies,
artifacts.extraClassPath,
artifacts.extraSourceJars,
allArtifacts.flatMap(_.userDependencies),
allArtifacts.flatMap(_.extraClassPath),
allArtifacts.flatMap(_.extraSourceJars),
value(options.finalRepositories),
logger,
cache,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import caseapp.core.help.Help
import scala.cli.commands.shared.{
CrossOptions,
HelpGroup,
ScopeOptions,
SharedJavaOptions,
SharedPythonOptions,
SharedWatchOptions
Expand Down Expand Up @@ -48,7 +49,10 @@ final case class SharedReplOptions(
@Hidden
@Tag(tags.implementation)
@HelpMessage("Don't actually run the REPL, just fetch it")
replDryRun: Boolean = false
replDryRun: Boolean = false,

@Recurse
scope: ScopeOptions = ScopeOptions()
)
// format: on

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package scala.cli.commands.shared

import caseapp.*

import scala.cli.commands.tags

case class ScopeOptions(
@Group(HelpGroup.Compilation.toString)
@HelpMessage("Include test scope")
@Tag(tags.should)
@Tag(tags.inShortHelp)
@Name("testScope")
@Name("withTestScope")
@Name("withTest")
test: Boolean = false
)
object ScopeOptions {
implicit lazy val parser: Parser[ScopeOptions] = Parser.derive
implicit lazy val help: Help[ScopeOptions] = Help.derive
}
Loading

0 comments on commit 2c1a420

Please sign in to comment.