diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index db9ff12d53b..51409d48ba7 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -699,6 +699,7 @@ class MetalsLspService( statusBar, sourceMapper, userConfig, + testProvider, ) ) diff --git a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala index f6987750ca7..e72c9df1bed 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala @@ -47,6 +47,7 @@ import scala.meta.internal.metals.clients.language.MetalsQuickPickParams import scala.meta.internal.metals.clients.language.MetalsStatusParams import scala.meta.internal.metals.config.RunType import scala.meta.internal.metals.config.RunType._ +import scala.meta.internal.metals.testProvider.TestSuitesProvider import scala.meta.internal.mtags.DefinitionAlternatives.GlobalSymbol import scala.meta.internal.mtags.OnDemandSymbolIndex import scala.meta.internal.mtags.Semanticdbs @@ -84,6 +85,7 @@ class DebugProvider( statusBar: StatusBar, sourceMapper: SourceMapper, userConfig: () => UserConfiguration, + testProvider: TestSuitesProvider, ) extends Cancelable { import DebugProvider._ @@ -634,13 +636,20 @@ class DebugProvider( )(implicit ec: ExecutionContext): Future[DebugSessionParams] = { def makeDebugSession() = { val debugSession = - if (supportsTestSelection(request.target)) + if (supportsTestSelection(request.target)) { + val testSuites = + request.requestData.copy(suites = request.requestData.suites.map { + suite => + if (testProvider.getFramework(buildTarget, suite) == JUnit4) + suite.copy(tests = suite.tests.map(_.replace("$", "\\$"))) + else suite + }) new b.DebugSessionParams( singletonList(buildTarget.getId), DebugProvider.ScalaTestSelection, - request.requestData.toJson, + testSuites.toJson, ) - else + } else new b.DebugSessionParams( singletonList(buildTarget.getId), b.DebugSessionParamsDataKind.SCALA_TEST_SUITES, diff --git a/metals/src/main/scala/scala/meta/internal/metals/testProvider/BuildTargetUpdate.scala b/metals/src/main/scala/scala/meta/internal/metals/testProvider/BuildTargetUpdate.scala index b24916dac77..b23cf4ccb14 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/testProvider/BuildTargetUpdate.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/testProvider/BuildTargetUpdate.scala @@ -68,5 +68,13 @@ object TestExplorerEvent { // Represents a single test within a test suite final case class TestCaseEntry( name: String, + displayName: String, location: l.Location, ) + +object TestCaseEntry { + def apply( + name: String, + location: l.Location, + ): TestCaseEntry = TestCaseEntry(name, name, location) +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/testProvider/TestSuitesIndex.scala b/metals/src/main/scala/scala/meta/internal/metals/testProvider/TestSuitesIndex.scala index a5931307ee9..b69e21202fb 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/testProvider/TestSuitesIndex.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/testProvider/TestSuitesIndex.scala @@ -127,6 +127,9 @@ private[testProvider] final class TestSuitesIndex { ): Set[FullyQualifiedName] = cachedTestSuites.get(buildTarget).map(_.keySet.toSet).getOrElse(Set.empty) + def get(target: BuildTarget, name: FullyQualifiedName): Option[TestEntry] = + cachedTestSuites.get(target).flatMap(_.get(name)) + def remove( buildTarget: BuildTarget, suiteName: FullyQualifiedName, diff --git a/metals/src/main/scala/scala/meta/internal/metals/testProvider/TestSuitesProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/testProvider/TestSuitesProvider.scala index a835578a883..5f4fbccea55 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/testProvider/TestSuitesProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/testProvider/TestSuitesProvider.scala @@ -24,6 +24,7 @@ import scala.meta.internal.metals.debug.BuildTargetClasses import scala.meta.internal.metals.debug.JUnit4 import scala.meta.internal.metals.debug.MUnit import scala.meta.internal.metals.debug.Scalatest +import scala.meta.internal.metals.debug.TestFramework import scala.meta.internal.metals.debug.Unknown import scala.meta.internal.metals.testProvider.TestExplorerEvent._ import scala.meta.internal.metals.testProvider.frameworks.JunitTestFinder @@ -535,4 +536,15 @@ final class TestSuitesProvider( events, ) + def getFramework( + target: BuildTarget, + selection: ScalaTestSuiteSelection, + ): TestFramework = { + val framework = + for { + testEntry <- index.get(target, FullyQualifiedName(selection.className)) + } yield testEntry.suiteDetails.framework + framework.getOrElse(Unknown) + } + } diff --git a/metals/src/main/scala/scala/meta/internal/metals/testProvider/frameworks/JunitTestFinder.scala b/metals/src/main/scala/scala/meta/internal/metals/testProvider/frameworks/JunitTestFinder.scala index 474031f7834..da6ea3ebae7 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/testProvider/frameworks/JunitTestFinder.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/testProvider/frameworks/JunitTestFinder.scala @@ -1,5 +1,7 @@ package scala.meta.internal.metals.testProvider.frameworks +import scala.reflect.NameTransformer + import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.testProvider.TestCaseEntry import scala.meta.internal.mtags @@ -37,7 +39,14 @@ class JunitTestFinder { case symbol if isValid(symbol) => doc .toLocation(uri, symbol.symbol) - .map(location => TestCaseEntry(symbol.displayName, location)) + .map { location => + val encodedName = NameTransformer.encode(symbol.displayName) + TestCaseEntry( + encodedName, + symbol.displayName, + location, + ) + } } .flatten .toVector diff --git a/tests/unit/src/test/scala/tests/testProvider/TestSuitesProviderSuite.scala b/tests/unit/src/test/scala/tests/testProvider/TestSuitesProviderSuite.scala index 9899728efa1..47007e2cc5b 100644 --- a/tests/unit/src/test/scala/tests/testProvider/TestSuitesProviderSuite.scala +++ b/tests/unit/src/test/scala/tests/testProvider/TestSuitesProviderSuite.scala @@ -538,6 +538,52 @@ class TestSuitesProviderSuite extends BaseLspSuite("testSuitesFinderSuite") { }, ) + checkEvents( + "check-backtick", + List("junit:junit:4.13.2", "com.github.sbt:junit-interface:0.13.3"), + s"""| + |/app/src/main/scala/JunitTestSuite.scala + |import org.junit.Test + |class JunitTestSuite { + | @Test + | def `test-backtick` = () + |} + |""".stripMargin, + file = "app/src/main/scala/JunitTestSuite.scala", + expected = () => { + val fcqn = "JunitTestSuite" + val className = "JunitTestSuite" + val symbol = "_empty_/JunitTestSuite#" + val file = "app/src/main/scala/JunitTestSuite.scala" + List( + rootBuildTargetUpdate( + "app", + targetUri, + List[TestExplorerEvent]( + AddTestSuite( + fcqn, + className, + symbol, + QuickLocation(classUriFor(file), (1, 6, 1, 20)).toLsp, + canResolveChildren = true, + ), + AddTestCases( + fcqn, + className, + List( + TestCaseEntry( + "test$minusbacktick", + "test-backtick", + QuickLocation(classUriFor(file), (3, 6, 3, 21)).toLsp, + ) + ).asJava, + ), + ).asJava, + ) + ) + }, + ) + checkEvents( "scalatest-any-fun-suite", List("org.scalatest::scalatest:3.2.13"),