Skip to content

Commit 1282b2e

Browse files
committed
Include test scope in the REPL when the --test flag is passed
1 parent acfcc17 commit 1282b2e

File tree

12 files changed

+175
-99
lines changed

12 files changed

+175
-99
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: 79 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,21 @@ 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+
buildOptions = builds.map(_.options).reduce(_ orElse _),
161+
allArtifacts = builds.map(_.artifacts),
162+
mainJarsOrClassDirs =
163+
if (asJar) builds.map(Library.libraryJar(_)) else builds.map(_.output),
164+
allowExit = allowExit,
165+
runMode = runMode,
166+
successfulBuilds = builds
171167
)
168+
}
172169

173170
val cross = options.sharedRepl.compileCross.cross.getOrElse(false)
174171
val configDb = ConfigDbUtils.configDb.orExit(logger)
@@ -178,18 +175,22 @@ object Repl extends ScalaCommand[ReplOptions] {
178175
)
179176

180177
if (inputs.isEmpty) {
181-
val artifacts = initialBuildOptions.artifacts(logger, Scope.Main).orExit(logger)
178+
val allArtifacts =
179+
Seq(initialBuildOptions.artifacts(logger, Scope.Main).orExit(logger)) ++
180+
(if options.sharedRepl.scope.test
181+
then Seq(initialBuildOptions.artifacts(logger, Scope.Test).orExit(logger))
182+
else Nil)
182183
// synchronizing, so that multiple presses to enter (handled by WatchUtil.waitForCtrlC)
183184
// don't try to run repls in parallel
184185
val lock = new Object
185186
def runThing() = lock.synchronized {
186187
doRunRepl(
187-
initialBuildOptions,
188-
artifacts,
189-
None,
188+
buildOptions = initialBuildOptions,
189+
allArtifacts = allArtifacts,
190+
mainJarsOrClassDirs = Seq.empty,
190191
allowExit = !options.sharedRepl.watch.watchMode,
191192
runMode = runMode(options),
192-
buildOpt = None
193+
successfulBuilds = Seq.empty
193194
)
194195
}
195196
runThing()
@@ -207,22 +208,20 @@ object Repl extends ScalaCommand[ReplOptions] {
207208
None,
208209
logger,
209210
crossBuilds = cross,
210-
buildTests = false,
211+
buildTests = options.sharedRepl.scope.test,
211212
partial = None,
212213
actionableDiagnostics = actionableDiagnostics,
213214
postAction = () => WatchUtil.printWatchMessage()
214215
) { res =>
215216
for (builds <- res.orReport(logger))
216-
builds.main match {
217-
case s: Build.Successful =>
217+
postBuild(builds, allowExit = false) {
218+
successfulBuilds =>
218219
doRunReplFromBuild(
219-
s,
220+
successfulBuilds,
220221
allowExit = false,
221222
runMode = runMode(options),
222223
asJar = options.shared.asJar
223224
)
224-
case _: Build.Failed => buildFailed(allowExit = false)
225-
case _: Build.Cancelled => buildCancelled(allowExit = false)
226225
}
227226
}
228227
try WatchUtil.waitForCtrlC(() => watcher.schedule())
@@ -237,25 +236,35 @@ object Repl extends ScalaCommand[ReplOptions] {
237236
None,
238237
logger,
239238
crossBuilds = cross,
240-
buildTests = false,
239+
buildTests = options.sharedRepl.scope.test,
241240
partial = None,
242241
actionableDiagnostics = actionableDiagnostics
243242
)
244243
.orExit(logger)
245-
builds.main match {
246-
case s: Build.Successful =>
244+
postBuild(builds, allowExit = false) {
245+
successfulBuilds =>
247246
doRunReplFromBuild(
248-
s,
247+
successfulBuilds,
249248
allowExit = true,
250249
runMode = runMode(options),
251250
asJar = options.shared.asJar
252251
)
253-
case _: Build.Failed => buildFailed(allowExit = true)
254-
case _: Build.Cancelled => buildCancelled(allowExit = true)
255252
}
256253
}
257254
}
258255

256+
def postBuild(builds: Builds, allowExit: Boolean)(f: Seq[Build.Successful] => Unit): Unit = {
257+
if builds.anyBuildFailed then {
258+
System.err.println("Compilation failed")
259+
if allowExit then sys.exit(1)
260+
}
261+
else if builds.anyBuildCancelled then {
262+
System.err.println("Build cancelled")
263+
if allowExit then sys.exit(1)
264+
}
265+
else f((Seq(builds.main) ++ builds.get(Scope.Test).toSeq).map(_.asInstanceOf[Build.Successful]))
266+
}
267+
259268
private def maybeAdaptForWindows(args: Seq[String]): Seq[String] =
260269
if (Properties.isWin)
261270
args.map { a =>
@@ -268,24 +277,28 @@ object Repl extends ScalaCommand[ReplOptions] {
268277
private def runRepl(
269278
options: BuildOptions,
270279
programArgs: Seq[String],
271-
artifacts: Artifacts,
272-
mainJarOrClassDir: Option[os.Path],
280+
allArtifacts: Seq[Artifacts],
281+
mainJarsOrClassDirs: Seq[os.Path],
273282
directories: scala.build.Directories,
274283
logger: Logger,
275284
allowExit: Boolean,
276285
dryRun: Boolean,
277286
runMode: RunMode.HasRepl,
278-
buildOpt: Option[Build.Successful]
287+
successfulBuilds: Seq[Build.Successful]
279288
): Either[BuildException, Unit] = either {
280289

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

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

286-
val scalaParams = artifacts.scalaOpt match {
287-
case Some(artifacts) => artifacts.params
288-
case None => ScalaParameters(Constants.defaultScalaVersion)
295+
val scalaParams: ScalaParameters = value {
296+
val distinctScalaArtifacts = allArtifacts.flatMap(_.scalaOpt).distinctBy(_.params)
297+
if distinctScalaArtifacts.isEmpty then
298+
Right(ScalaParameters(Constants.defaultScalaVersion))
299+
else if distinctScalaArtifacts.length == 1 then
300+
Right(distinctScalaArtifacts.head.params)
301+
else Left(MultipleScalaVersionsError(distinctScalaArtifacts.map(_.params.scalaVersion)))
289302
}
290303

291304
val (scalapyJavaOpts, scalapyExtraEnv) =
@@ -302,7 +315,7 @@ object Repl extends ScalaCommand[ReplOptions] {
302315
// Putting current dir in PYTHONPATH, see
303316
// https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174
304317
// for context.
305-
val dirs = buildOpt.map(_.inputs.workspace).toSeq ++ Seq(os.pwd)
318+
val dirs = successfulBuilds.map(_.inputs.workspace) ++ Seq(os.pwd)
306319
(props0, pythonPathEnv(dirs: _*))
307320
}
308321
else
@@ -338,15 +351,14 @@ object Repl extends ScalaCommand[ReplOptions] {
338351

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

341-
val rootClasses = mainJarOrClassDir match {
342-
case None => Nil
343-
case Some(dir) if os.isDir(dir) =>
354+
val rootClasses = mainJarsOrClassDirs.flatMap {
355+
case dir if os.isDir(dir) =>
344356
os.list(dir)
345357
.filter(_.last.endsWith(".class"))
346358
.filter(os.isFile(_)) // just in case
347359
.map(_.last.stripSuffix(".class"))
348360
.sorted
349-
case Some(jar) =>
361+
case jar =>
350362
var zf: ZipFile = null
351363
try {
352364
zf = new ZipFile(jar.toIO)
@@ -396,7 +408,7 @@ object Repl extends ScalaCommand[ReplOptions] {
396408
replArtifacts.replJavaOpts ++
397409
options.javaOptions.javaOpts.toSeq.map(_.value.value) ++
398410
extraProps.toVector.sorted.map { case (k, v) => s"-D$k=$v" },
399-
classPath = mainJarOrClassDir.toSeq ++ replArtifacts.replClassPath,
411+
classPath = mainJarsOrClassDirs ++ replArtifacts.replClassPath,
400412
mainClass = replArtifacts.replMainClass,
401413
args = maybeAdaptForWindows(depClassPathArgs ++ replArgs),
402414
logger = logger,
@@ -411,8 +423,8 @@ object Repl extends ScalaCommand[ReplOptions] {
411423
value {
412424
ReplArtifacts.default(
413425
scalaParams,
414-
artifacts.userDependencies,
415-
artifacts.extraClassPath,
426+
allArtifacts.flatMap(_.userDependencies).distinct,
427+
allArtifacts.flatMap(_.extraClassPath).distinct,
416428
logger,
417429
cache,
418430
value(options.finalRepositories),
@@ -427,9 +439,9 @@ object Repl extends ScalaCommand[ReplOptions] {
427439
ReplArtifacts.ammonite(
428440
scalaParams,
429441
options.notForBloopOptions.replOptions.ammoniteVersion(scalaParams.scalaVersion, logger),
430-
artifacts.userDependencies,
431-
artifacts.extraClassPath,
432-
artifacts.extraSourceJars,
442+
allArtifacts.flatMap(_.userDependencies),
443+
allArtifacts.flatMap(_.extraClassPath),
444+
allArtifacts.flatMap(_.extraSourceJars),
433445
value(options.finalRepositories),
434446
logger,
435447
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)