Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to run everything in the current file #3311

Merged
merged 1 commit into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions docs/integrations/debug-adapter-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,15 @@ a-b-key9=value 9 # will be ignored

This option works a bit different than the other two param shapes as you don't
specify a test or main class, but rather a `runType` of either `"run"`,
`"testFile"`, or `"testTarget"` and a file URI representing your current location.
`"run"` will automatically find any main method in the build target that belongs
to the URI that was sent in. If multiple are found, you will be given the choice
of which to run. The `"testFile"` option will check for any test classes in your
current file and run them. Similarly, `"testTarget"` will run all test classes
found in the build target that the URI belongs to. The `"args"`, `"jvmOptions"`,
`"env"`, and `"envFile"` are all valid keys that can be sent as well with the
same format as above.
`"runOrTestFile"`, `"testFile"`, or `"testTarget"` and a file URI representing
your current location. `"run"` will automatically find any main method in the
build target that belongs to the URI that was sent in. If multiple are found,
you will be given the choice of which to run. `"runOrTestFile"` will try to find
a main or test class in your current file and run them. The `"testFile"` option
will check for any test classes in your current file and run them. Similarly,
`"testTarget"` will run all test classes found in the build target that the URI
belongs to. The `"args"`, `"jvmOptions"`, `"env"`, and `"envFile"` are all valid
keys that can be sent as well with the same format as above.

```json
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package scala.meta.internal.metals.config
object RunType {
sealed trait RunType
case object Run extends RunType
case object RunOrTestFile extends RunType
case object TestFile extends RunType
case object TestTarget extends RunType

def fromString(string: String): Option[RunType] = {
string match {
case "run" => Some(Run)
case "runOrTestFile" => Some(RunOrTestFile)
case "testFile" => Some(TestFile)
case "testTarget" => Some(TestTarget)
case _ => None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import java.util.Collections.singletonList
import java.util.concurrent.TimeUnit
import java.{util => ju}

import scala.collection.concurrent.TrieMap
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.Promise
Expand Down Expand Up @@ -275,6 +276,51 @@ class DebugProvider(
}
}

private def resolveInFile(
buildTarget: BuildTargetIdentifier,
classes: TrieMap[
BuildTargetClasses.Symbol,
ScalaMainClass
],
testClasses: TrieMap[
BuildTargetClasses.Symbol,
BuildTargetClasses.ClassName
],
params: DebugDiscoveryParams
)(implicit ec: ExecutionContext) = {
val path = params.path.toAbsolutePath
semanticdbs
.textDocument(path)
.documentIncludingStale
.fold[Future[DebugSessionParams]] {
Future.failed(SemanticDbNotFoundException)
} { textDocument =>
lazy val tests = for {
symbolInfo <- textDocument.symbols
symbol = symbolInfo.symbol
testClass <- testClasses.get(symbol)
} yield testClass
val mains = for {
symbolInfo <- textDocument.symbols
symbol = symbolInfo.symbol
mainClass <- classes.get(symbol)
} yield mainClass
if (mains.nonEmpty) {
verifyMain(buildTarget, mains.toList, params)
} else if (tests.nonEmpty) {
Future(
new b.DebugSessionParams(
singletonList(buildTarget),
b.DebugSessionParamsDataKind.SCALA_TEST_SUITES,
tests.asJava.toJson
)
)
} else {
Future.failed(NoRunOptionException)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Total aside, but on my TODO list to to refactor all this fails out. I'm not a huge fan of using failing futures for control flow. I think we can probably model all of these differently. One day 😆

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you talking about a dedicated error channel?

}
}
}

/**
* Given fully unresolved params this figures out the runType that was passed
* in and then discovers either the main methods for the build target the
Expand All @@ -288,7 +334,7 @@ class DebugProvider(
val buildTargetO = buildTargets.inverseSources(path)

lazy val mainClasses = (bti: BuildTargetIdentifier) =>
buildTargetClasses.classesOf(bti).mainClasses.values.toList
buildTargetClasses.classesOf(bti).mainClasses

lazy val testClasses = (bti: BuildTargetIdentifier) =>
buildTargetClasses.classesOf(bti).testClasses
Expand All @@ -301,7 +347,9 @@ class DebugProvider(
case (None, _) =>
Future.failed(RunType.UnknownRunTypeException(params.runType))
case (Some(Run), Some(target)) =>
verifyMain(target, mainClasses(target), params)
verifyMain(target, mainClasses(target).values.toList, params)
case (Some(RunOrTestFile), Some(target)) =>
resolveInFile(target, mainClasses(target), testClasses(target), params)
case (Some(TestFile), Some(target)) if testClasses(target).isEmpty =>
Future.failed(
NoTestsFoundException("file", path.toString())
Expand Down Expand Up @@ -629,6 +677,10 @@ case object WorkspaceErrorsException
extends Exception(
s"Cannot run class, since the workspace has errors."
)
case object NoRunOptionException
extends Exception(
s"There is nothing to run or test in the current file."
)
case object SemanticDbNotFoundException
extends Exception(
"Build misconfiguration. No semanticdb can be found for you file, please check the doctor."
Expand Down
66 changes: 66 additions & 0 deletions tests/unit/src/test/scala/tests/DebugDiscoverySuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,72 @@ class DebugDiscoverySuite
} yield assertNoDiff(output, "oranges are nice")
}

test("run-file-main") {
for {
_ <- initialize(
s"""/metals.json
|{
| "a": {}
|}
|/${mainPath}
|package a
|object Main {
| def main(args: Array[String]) = {
| print("oranges are nice")
| System.exit(0)
| }
|}
|""".stripMargin
)
_ <- server.didOpen(mainPath)
_ <- server.waitFor(TimeUnit.SECONDS.toMillis(10))
debugger <- server.startDebuggingUnresolved(
new DebugDiscoveryParams(
server.toPath(mainPath).toURI.toString,
"runOrTestFile"
).toJson
)
_ <- debugger.initialize
_ <- debugger.launch
_ <- debugger.configurationDone
_ <- debugger.shutdown
output <- debugger.allOutput
} yield assertNoDiff(output, "oranges are nice")
}

test("run-file-test") {
for {
_ <- initialize(
s"""/metals.json
|{
| "a": {
| "libraryDependencies":["org.scalatest::scalatest:3.0.5"]
| }
|}
|/${fooPath}
|package a
|class Foo extends org.scalatest.FunSuite {
| test("foo") {}
|}
|""".stripMargin
)
_ <- server.didOpen(fooPath)
_ <- server.didSave(fooPath)(identity)
_ <- server.waitFor(TimeUnit.SECONDS.toMillis(10))
debugger <- server.startDebuggingUnresolved(
new DebugDiscoveryParams(
server.toPath(fooPath).toURI.toString,
"runOrTestFile"
).toJson
)
_ <- debugger.initialize
_ <- debugger.launch
_ <- debugger.configurationDone
_ <- debugger.shutdown
output <- debugger.allOutput
} yield assert(output.contains("All tests in a.Foo passed"))
}

test("run-multiple") {
for {
_ <- initialize(
Expand Down