Skip to content

Commit 753b099

Browse files
authored
Include test scope in the REPL when the --test flag is passed (#2971)
1 parent 0425573 commit 753b099

File tree

14 files changed

+259
-100
lines changed

14 files changed

+259
-100
lines changed

modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers {
109109
None,
110110
logger,
111111
crossBuilds = cross,
112-
buildTests = options.test,
112+
buildTests = options.scope.test,
113113
partial = None,
114114
actionableDiagnostics = actionableDiagnostics,
115115
postAction = () => WatchUtil.printWatchMessage()
@@ -128,7 +128,7 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers {
128128
None,
129129
logger,
130130
crossBuilds = cross,
131-
buildTests = options.test,
131+
buildTests = options.scope.test,
132132
partial = None,
133133
actionableDiagnostics = actionableDiagnostics
134134
)

modules/cli/src/main/scala/scala/cli/commands/compile/CompileOptions.scala

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,7 @@ package scala.cli.commands.compile
33
import caseapp.*
44
import caseapp.core.help.Help
55

6-
import scala.cli.commands.shared.{
7-
CrossOptions,
8-
HasSharedOptions,
9-
HelpGroup,
10-
HelpMessages,
11-
SharedOptions,
12-
SharedWatchOptions
13-
}
6+
import scala.cli.commands.shared._
147
import scala.cli.commands.tags
158

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

34-
@Group(HelpGroup.Compilation.toString)
35-
@HelpMessage("Compile test scope")
36-
@Tag(tags.should)
37-
@Tag(tags.inShortHelp)
38-
test: Boolean = false
27+
@Recurse
28+
scope: ScopeOptions = ScopeOptions()
3929
) extends HasSharedOptions
4030
// format: on
4131

modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala

Lines changed: 81 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import java.util.zip.ZipFile
1212

1313
import scala.build.EitherCps.{either, value}
1414
import scala.build.*
15-
import scala.build.errors.{BuildException, CantDownloadAmmoniteError, FetchingDependenciesError}
15+
import scala.build.errors.{
16+
BuildException,
17+
CantDownloadAmmoniteError,
18+
FetchingDependenciesError,
19+
MultipleScalaVersionsError
20+
}
1621
import scala.build.input.Inputs
1722
import scala.build.internal.{Constants, Runner}
1823
import scala.build.options.{BuildOptions, JavaOpt, MaybeScalaVersion, Scope}
@@ -24,6 +29,7 @@ import scala.cli.commands.run.Run.{
2429
}
2530
import scala.cli.commands.run.RunMode
2631
import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions}
32+
import scala.cli.commands.util.BuildCommandHelpers
2733
import scala.cli.commands.{ScalaCommand, WatchUtil}
2834
import scala.cli.config.{ConfigDb, Keys}
2935
import scala.cli.packaging.Library
@@ -33,7 +39,7 @@ import scala.cli.{CurrentParams, ScalaCli}
3339
import scala.jdk.CollectionConverters.*
3440
import scala.util.Properties
3541

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

118124
val directories = Directories.directories
119125

120-
def buildFailed(allowExit: Boolean): Unit = {
121-
System.err.println("Compilation failed")
122-
if (allowExit)
123-
sys.exit(1)
124-
}
125-
def buildCancelled(allowExit: Boolean): Unit = {
126-
System.err.println("Build cancelled")
127-
if (allowExit)
128-
sys.exit(1)
129-
}
130-
131126
def doRunRepl(
132127
buildOptions: BuildOptions,
133-
artifacts: Artifacts,
134-
mainJarOrClassDir: Option[os.Path],
128+
allArtifacts: Seq[Artifacts],
129+
mainJarsOrClassDirs: Seq[os.Path],
135130
allowExit: Boolean,
136131
runMode: RunMode.HasRepl,
137-
buildOpt: Option[Build.Successful]
132+
successfulBuilds: Seq[Build.Successful]
138133
): Unit = {
139134
val res = runRepl(
140-
buildOptions,
141-
programArgs,
142-
artifacts,
143-
mainJarOrClassDir,
144-
directories,
145-
logger,
135+
options = buildOptions,
136+
programArgs = programArgs,
137+
allArtifacts = allArtifacts,
138+
mainJarsOrClassDirs = mainJarsOrClassDirs,
139+
directories = directories,
140+
logger = logger,
146141
allowExit = allowExit,
147-
options.sharedRepl.replDryRun,
148-
runMode,
149-
buildOpt
142+
dryRun = options.sharedRepl.replDryRun,
143+
runMode = runMode,
144+
successfulBuilds = successfulBuilds
150145
)
151146
res match {
152147
case Left(ex) =>
@@ -156,19 +151,23 @@ object Repl extends ScalaCommand[ReplOptions] {
156151
}
157152
}
158153
def doRunReplFromBuild(
159-
build: Build.Successful,
154+
builds: Seq[Build.Successful],
160155
allowExit: Boolean,
161156
runMode: RunMode.HasRepl,
162157
asJar: Boolean
163-
): Unit =
158+
): Unit = {
164159
doRunRepl(
165-
build.options,
166-
build.artifacts,
167-
Some(if (asJar) Library.libraryJar(build) else build.output),
168-
allowExit,
169-
runMode,
170-
Some(build)
160+
// build options should be the same for both scopes
161+
// combining them may cause for ammonite args to be duplicated, so we're using the main scope's opts
162+
buildOptions = builds.head.options,
163+
allArtifacts = builds.map(_.artifacts),
164+
mainJarsOrClassDirs =
165+
if (asJar) builds.map(Library.libraryJar(_)) else builds.map(_.output),
166+
allowExit = allowExit,
167+
runMode = runMode,
168+
successfulBuilds = builds
171169
)
170+
}
172171

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

180179
if (inputs.isEmpty) {
181-
val artifacts = initialBuildOptions.artifacts(logger, Scope.Main).orExit(logger)
180+
val allArtifacts =
181+
Seq(initialBuildOptions.artifacts(logger, Scope.Main).orExit(logger)) ++
182+
(if options.sharedRepl.scope.test
183+
then Seq(initialBuildOptions.artifacts(logger, Scope.Test).orExit(logger))
184+
else Nil)
182185
// synchronizing, so that multiple presses to enter (handled by WatchUtil.waitForCtrlC)
183186
// don't try to run repls in parallel
184187
val lock = new Object
185188
def runThing() = lock.synchronized {
186189
doRunRepl(
187-
initialBuildOptions,
188-
artifacts,
189-
None,
190+
buildOptions = initialBuildOptions,
191+
allArtifacts = allArtifacts,
192+
mainJarsOrClassDirs = Seq.empty,
190193
allowExit = !options.sharedRepl.watch.watchMode,
191194
runMode = runMode(options),
192-
buildOpt = None
195+
successfulBuilds = Seq.empty
193196
)
194197
}
195198
runThing()
@@ -207,22 +210,20 @@ object Repl extends ScalaCommand[ReplOptions] {
207210
None,
208211
logger,
209212
crossBuilds = cross,
210-
buildTests = false,
213+
buildTests = options.sharedRepl.scope.test,
211214
partial = None,
212215
actionableDiagnostics = actionableDiagnostics,
213216
postAction = () => WatchUtil.printWatchMessage()
214217
) { res =>
215218
for (builds <- res.orReport(logger))
216-
builds.main match {
217-
case s: Build.Successful =>
219+
postBuild(builds, allowExit = false) {
220+
successfulBuilds =>
218221
doRunReplFromBuild(
219-
s,
222+
successfulBuilds,
220223
allowExit = false,
221224
runMode = runMode(options),
222225
asJar = options.shared.asJar
223226
)
224-
case _: Build.Failed => buildFailed(allowExit = false)
225-
case _: Build.Cancelled => buildCancelled(allowExit = false)
226227
}
227228
}
228229
try WatchUtil.waitForCtrlC(() => watcher.schedule())
@@ -237,25 +238,35 @@ object Repl extends ScalaCommand[ReplOptions] {
237238
None,
238239
logger,
239240
crossBuilds = cross,
240-
buildTests = false,
241+
buildTests = options.sharedRepl.scope.test,
241242
partial = None,
242243
actionableDiagnostics = actionableDiagnostics
243244
)
244245
.orExit(logger)
245-
builds.main match {
246-
case s: Build.Successful =>
246+
postBuild(builds, allowExit = false) {
247+
successfulBuilds =>
247248
doRunReplFromBuild(
248-
s,
249+
successfulBuilds,
249250
allowExit = true,
250251
runMode = runMode(options),
251252
asJar = options.shared.asJar
252253
)
253-
case _: Build.Failed => buildFailed(allowExit = true)
254-
case _: Build.Cancelled => buildCancelled(allowExit = true)
255254
}
256255
}
257256
}
258257

258+
def postBuild(builds: Builds, allowExit: Boolean)(f: Seq[Build.Successful] => Unit): Unit = {
259+
if builds.anyBuildFailed then {
260+
System.err.println("Compilation failed")
261+
if allowExit then sys.exit(1)
262+
}
263+
else if builds.anyBuildCancelled then {
264+
System.err.println("Build cancelled")
265+
if allowExit then sys.exit(1)
266+
}
267+
else f(builds.builds.sortBy(_.scope).map(_.asInstanceOf[Build.Successful]))
268+
}
269+
259270
private def maybeAdaptForWindows(args: Seq[String]): Seq[String] =
260271
if (Properties.isWin)
261272
args.map { a =>
@@ -268,24 +279,28 @@ object Repl extends ScalaCommand[ReplOptions] {
268279
private def runRepl(
269280
options: BuildOptions,
270281
programArgs: Seq[String],
271-
artifacts: Artifacts,
272-
mainJarOrClassDir: Option[os.Path],
282+
allArtifacts: Seq[Artifacts],
283+
mainJarsOrClassDirs: Seq[os.Path],
273284
directories: scala.build.Directories,
274285
logger: Logger,
275286
allowExit: Boolean,
276287
dryRun: Boolean,
277288
runMode: RunMode.HasRepl,
278-
buildOpt: Option[Build.Successful]
289+
successfulBuilds: Seq[Build.Successful]
279290
): Either[BuildException, Unit] = either {
280291

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

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

286-
val scalaParams = artifacts.scalaOpt match {
287-
case Some(artifacts) => artifacts.params
288-
case None => ScalaParameters(Constants.defaultScalaVersion)
297+
val scalaParams: ScalaParameters = value {
298+
val distinctScalaParams = allArtifacts.flatMap(_.scalaOpt).map(_.params).distinct
299+
if distinctScalaParams.isEmpty then
300+
Right(ScalaParameters(Constants.defaultScalaVersion))
301+
else if distinctScalaParams.length == 1 then
302+
Right(distinctScalaParams.head)
303+
else Left(MultipleScalaVersionsError(distinctScalaParams.map(_.scalaVersion)))
289304
}
290305

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

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

341-
val rootClasses = mainJarOrClassDir match {
342-
case None => Nil
343-
case Some(dir) if os.isDir(dir) =>
356+
val rootClasses = mainJarsOrClassDirs.flatMap {
357+
case dir if os.isDir(dir) =>
344358
os.list(dir)
345359
.filter(_.last.endsWith(".class"))
346360
.filter(os.isFile(_)) // just in case
347361
.map(_.last.stripSuffix(".class"))
348362
.sorted
349-
case Some(jar) =>
363+
case jar =>
350364
var zf: ZipFile = null
351365
try {
352366
zf = new ZipFile(jar.toIO)
@@ -396,7 +410,7 @@ object Repl extends ScalaCommand[ReplOptions] {
396410
replArtifacts.replJavaOpts ++
397411
options.javaOptions.javaOpts.toSeq.map(_.value.value) ++
398412
extraProps.toVector.sorted.map { case (k, v) => s"-D$k=$v" },
399-
classPath = mainJarOrClassDir.toSeq ++ replArtifacts.replClassPath,
413+
classPath = mainJarsOrClassDirs ++ replArtifacts.replClassPath,
400414
mainClass = replArtifacts.replMainClass,
401415
args = maybeAdaptForWindows(depClassPathArgs ++ replArgs),
402416
logger = logger,
@@ -411,8 +425,8 @@ object Repl extends ScalaCommand[ReplOptions] {
411425
value {
412426
ReplArtifacts.default(
413427
scalaParams,
414-
artifacts.userDependencies,
415-
artifacts.extraClassPath,
428+
allArtifacts.flatMap(_.userDependencies).distinct,
429+
allArtifacts.flatMap(_.extraClassPath).distinct,
416430
logger,
417431
cache,
418432
value(options.finalRepositories),
@@ -427,9 +441,9 @@ object Repl extends ScalaCommand[ReplOptions] {
427441
ReplArtifacts.ammonite(
428442
scalaParams,
429443
options.notForBloopOptions.replOptions.ammoniteVersion(scalaParams.scalaVersion, logger),
430-
artifacts.userDependencies,
431-
artifacts.extraClassPath,
432-
artifacts.extraSourceJars,
444+
allArtifacts.flatMap(_.userDependencies),
445+
allArtifacts.flatMap(_.extraClassPath),
446+
allArtifacts.flatMap(_.extraSourceJars),
433447
value(options.finalRepositories),
434448
logger,
435449
cache,

modules/cli/src/main/scala/scala/cli/commands/repl/SharedReplOptions.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import caseapp.core.help.Help
66
import scala.cli.commands.shared.{
77
CrossOptions,
88
HelpGroup,
9+
ScopeOptions,
910
SharedJavaOptions,
1011
SharedPythonOptions,
1112
SharedWatchOptions
@@ -48,7 +49,10 @@ final case class SharedReplOptions(
4849
@Hidden
4950
@Tag(tags.implementation)
5051
@HelpMessage("Don't actually run the REPL, just fetch it")
51-
replDryRun: Boolean = false
52+
replDryRun: Boolean = false,
53+
54+
@Recurse
55+
scope: ScopeOptions = ScopeOptions()
5256
)
5357
// format: on
5458

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package scala.cli.commands.shared
2+
3+
import caseapp.*
4+
5+
import scala.cli.commands.tags
6+
7+
case class ScopeOptions(
8+
@Group(HelpGroup.Compilation.toString)
9+
@HelpMessage("Include test scope")
10+
@Tag(tags.should)
11+
@Tag(tags.inShortHelp)
12+
@Name("testScope")
13+
@Name("withTestScope")
14+
@Name("withTest")
15+
test: Boolean = false
16+
)
17+
object ScopeOptions {
18+
implicit lazy val parser: Parser[ScopeOptions] = Parser.derive
19+
implicit lazy val help: Help[ScopeOptions] = Help.derive
20+
}

0 commit comments

Comments
 (0)