Skip to content

Support multi-project setups in the IDE tests #5058

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

Merged
merged 7 commits into from
Oct 19, 2018
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ class DefinitionTest {
.definition(m13 to m14, List(m3 to m4))
}

@Test def classDefinitionDifferentWorkspace: Unit = {
val w0 = Workspace.withSources(
code"""class ${m1}Foo${m2}"""
)

val w1 = Workspace.dependingOn(w0).withSources(
code"""class Bar { new ${m3}Foo${m4} }"""
)

val w2 = Workspace.dependingOn(w1).withSources(
code"""class Baz extends ${m5}Foo${m6}"""
)

withWorkspaces(w0, w1, w2)
.definition(m1 to m2, List(m1 to m2))
.definition(m3 to m4, List(m1 to m2))
.definition(m5 to m6, List(m1 to m2))
}

@Test def valDefinition0: Unit = {
withSources(
code"class Foo { val ${m1}x$m2 = 0; ${m3}x$m4 }",
Expand Down Expand Up @@ -189,4 +208,15 @@ class DefinitionTest {
.definition(m9 to m10, List(m3 to m4))
}

@Test def definitionFromTasty: Unit = {
withSources(
tasty"""package mypackage
class ${m1}A${m2}""",
code"""package mypackage
object O {
new ${m3}A${m4}
}"""
).definition(m3 to m4, List(m1 to m2))
}

}
82 changes: 76 additions & 6 deletions language-server/test/dotty/tools/languageserver/util/Code.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ object Code {
WorksheetWithPositions(text, positions)
}

/**
* An interpolator similar to `code`, but used for defining a source that will
* be unpickled from TASTY.
*
* @see code
*/
def tasty(args: Embedded*): TastyWithPositions = {
val (text, positions) = textAndPositions(args: _*)
TastyWithPositions(text, positions)
}

private def textAndPositions(args: Embedded*): (String, List[(CodeMarker, Int, Int)]) = {
val pi = sc.parts.iterator
val ai = args.iterator
Expand Down Expand Up @@ -99,19 +110,25 @@ object Code {
}
}

/** A new `CodeTester` working with `sources` in the workspace. */
def withSources(sources: SourceWithPositions*): CodeTester = new CodeTester(sources.toList, Nil)
/** A new `CodeTester` working with a single workspace containing `sources`. */
def withSources(sources: SourceWithPositions*): CodeTester = withWorkspaces(Workspace(sources.toList))

/** A new `CodeTester` working with `workspaces`. */
def withWorkspaces(workspaces: Workspace*): CodeTester = new CodeTester(workspaces.toList)

sealed trait SourceWithPositions {

/** The code contained within the virtual source file. */
/** A name for this source given its index. */
def sourceName(index: Int): String

/** The code contained within the virtual source file. */
def text: String

/** The positions of the markers that have been set. */
def positions: List[(CodeMarker, Int, Int)]

/** A new `CodeTester` with only this source in the workspace. */
def withSource: CodeTester = new CodeTester(this :: Nil, Nil)
def withSource: CodeTester = withSources(this)

}

Expand All @@ -121,14 +138,67 @@ object Code {
* @param text The code contained within the virtual source file.
* @param positions The positions of the markers that have been set.
*/
case class ScalaSourceWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions
case class ScalaSourceWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions {
def sourceName(index: Int): String = s"Source$index.scala"
}

/**
* A virtual worksheet where several markers have been set.
*
* @param text The code contained within the virtual source file.
* @param positions The positions of the markers that have been set.
*/
case class WorksheetWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions
case class WorksheetWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions {
def sourceName(index: Int): String = s"Worksheet$index.sc"
}

/**
* A virtual source file that will not be opened in the IDE, but instead unpickled from TASTY.
*
* @param text The code contained within the virtual source file.
* @param positions The positions of the markers that have been set.
*/
case class TastyWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions {
def sourceName(index: Int): String = s"Source-from-tasty-$index.scala"
}

/**
* A group of sources belonging to the same project.
*
* @param sources The sources that this workspace holds.
* @param name The name of this workspace
* @param dependsOn The other workspaces on which this workspace depend.
*/
case class Workspace(sources: List[SourceWithPositions],
name: String = Workspace.freshName,
dependsOn: List[Workspace] = Nil) {

/**
* Add `sources` to the sources of this workspace.
*/
def withSources(sources: SourceWithPositions*): Workspace = copy(sources = this.sources ::: sources.toList)

}

object Workspace {
private[this] val count = new java.util.concurrent.atomic.AtomicInteger()
private def freshName: String = s"workspace${count.incrementAndGet()}"

/**
* Creates a new workspace that depends on `workspaces`.
*
* @param workspaces The dependencies of the new workspace.
* @return An empty workspace with a dependency on the specified workspaces.
*/
def dependingOn(workspaces: Workspace*) = new Workspace(Nil, dependsOn = workspaces.toList)

/**
* Create a new workspace with the given sources.
*
* @param sources The sources to add to this workspace.
* @return a new workspace containing the specified sources.
*/
def withSources(sources: SourceWithPositions*): Workspace = new Workspace(sources.toList)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ import org.eclipse.lsp4j.{CompletionItemKind, DocumentHighlightKind}
* Simulates an LSP client for test in a workspace defined by `sources`.
*
* @param sources The list of sources in the workspace
* @param actions Unused
*/
class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) {
class CodeTester(workspaces: List[Workspace]) {

private val testServer = new TestServer(TestFile.testDir)
private val testServer = new TestServer(TestFile.testDir, workspaces)

private val sources = for { workspace <- workspaces
source <- workspace.sources } yield (workspace, source)

private val files =
for { workspace <- workspaces
(source, id) <- workspace.sources.zipWithIndex } yield source match {
case src @ TastyWithPositions(text, _) => testServer.openCode(text, workspace, src.sourceName(id), openInIDE = false)
case other => testServer.openCode(other.text, workspace, other.sourceName(id), openInIDE = true)
}

private val files = sources.zipWithIndex.map {
case (ScalaSourceWithPositions(text, _), i) => testServer.openCode(text, s"Source$i.scala")
case (WorksheetWithPositions(text, _), i) => testServer.openCode(text, s"Worksheet$i.sc")
}
private val positions: PositionContext = getPositions(files)

/**
Expand Down Expand Up @@ -158,7 +163,13 @@ class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) {
action.execute()(testServer, testServer.client, positions)
} catch {
case ex: AssertionError =>
val sourcesStr = sources.zip(files).map{ case (source, file) => "// " + file.file + "\n" + source.text}.mkString("\n")
val sourcesStr =
sources.zip(files).map {
case ((workspace, source), file) =>
s"""// ${file.file} in workspace ${workspace.name}
|${source.text}""".stripMargin
}.mkString(System.lineSeparator)

val msg =
s"""
|
Expand All @@ -177,7 +188,7 @@ class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) {
private def getPositions(files: List[TestFile]): PositionContext = {
val posSeq = {
for {
(code, file) <- sources.zip(files)
((_, code), file) <- sources.zip(files)
(position, line, char) <- code.positions
} yield position -> (file, line, char)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,75 @@
package dotty.tools.languageserver.util.server

import java.io.PrintWriter
import java.io.File.{pathSeparator, separator}
import java.net.URI
import java.nio.file.Path
import java.nio.file.{Files, Path}
import java.util

import dotty.tools.dotc.Main
import dotty.tools.dotc.reporting.{Reporter, ThrowingReporter}
import dotty.tools.io.Directory
import dotty.tools.languageserver.DottyLanguageServer
import dotty.tools.languageserver.util.Code.{TastyWithPositions, Workspace}
import org.eclipse.lsp4j.{ DidOpenTextDocumentParams, InitializeParams, InitializeResult, TextDocumentItem}

class TestServer(testFolder: Path) {
class TestServer(testFolder: Path, workspaces: List[Workspace]) {

val server = new DottyLanguageServer
var client: TestClient = _

init()

private[this] def init(): InitializeResult = {
// Fill the configuration with values populated by sbt
def showSeq[T](lst: Seq[T]): String =
lst
.map(elem => '"' + elem.toString.replace('\\', '/') + '"')
.mkString("[ ", ", ", " ]")
val dottyIdeJson: String =
s"""[ {
| "id" : "dotty-ide-test",
var compiledWorkspaces: Set[Workspace] = Set.empty

/** Compile the dependencies of the given workspace, and then the workspace. */
def compileWorkspaceAndDependencies(workspace: Workspace): Unit =
if (!compiledWorkspaces.contains(workspace)) {
workspace.dependsOn.foreach(compileWorkspaceAndDependencies)
compileWorkspace(workspace)
compiledWorkspaces += workspace
}

/**
* Set up given workspace, return JSON config.
*
* If the workspace has dependencies, these dependencies are compiled. The classfiles of the
* dependent workspaces are put on the classpath of this workspace.
*
* @param workspace The workspace to configure.
* @return A JSON object representing the configuration for this workspace.
*/
def workspaceSetup(workspace: Workspace): String = {
def showSeq[T](lst: Seq[T]): String =
lst
.map(elem => '"' + elem.toString.replace('\\', '/') + '"')
.mkString("[ ", ", ", " ]")

if (workspace.sources.exists(_.isInstanceOf[TastyWithPositions])) {
compileWorkspaceAndDependencies(workspace)
} else {
// Compile all the dependencies of this workspace
workspace.dependsOn.foreach(compileWorkspaceAndDependencies)
}

s"""{
| "id" : "${workspace.name}",
| "compilerVersion" : "${BuildInfo.ideTestsCompilerVersion}",
| "compilerArguments" : ${showSeq(BuildInfo.ideTestsCompilerArguments)},
| "sourceDirectories" : ${showSeq(BuildInfo.ideTestsSourceDirectories)},
| "dependencyClasspath" : ${showSeq(BuildInfo.ideTestsDependencyClasspath)},
| "classDirectory" : "${BuildInfo.ideTestsClassDirectory.toString.replace('\\','/')}"
| "sourceDirectories" : ${showSeq(sourceDirectory(workspace, wipe = false) :: Nil)},
| "dependencyClasspath" : ${showSeq(dependencyClasspath(workspace))},
| "classDirectory" : "${classDirectory(workspace, wipe = false).toString.replace('\\','/')}"
|}
|]""".stripMargin
|""".stripMargin
}

Files.createDirectories(testFolder)
val configFile = testFolder.resolve(DottyLanguageServer.IDE_CONFIG_FILE)
testFolder.toFile.mkdirs()
testFolder.resolve("src").toFile.mkdirs()
testFolder.resolve("out").toFile.mkdirs()
val configuration = workspaces.map(workspaceSetup).mkString("[", ",", "]")

new PrintWriter(configFile.toString) {
write(dottyIdeJson)
write(configuration)
close()
}

Expand All @@ -52,17 +84,71 @@ class TestServer(testFolder: Path) {
/** Open the code in the given file and returns the file.
* @param code code in file
* @param fileName file path in the source directory
* @param openInIDE If true, send `textDocument/didOpen` to the server.
* @return the file opened
*/
def openCode(code: String, fileName: String): TestFile = {
val testFile = new TestFile(fileName)
val dotdp = new DidOpenTextDocumentParams()
def openCode(code: String, workspace: Workspace, fileName: String, openInIDE: Boolean): TestFile = {
val testFile = new TestFile(workspace.name + separator + fileName)
val tdi = new TextDocumentItem()
tdi.setUri(testFile.uri)
tdi.setText(code)
dotdp.setTextDocument(tdi)
server.didOpen(dotdp)

if (openInIDE) {
val dotdp = new DidOpenTextDocumentParams()
dotdp.setTextDocument(tdi)
server.didOpen(dotdp)
}

testFile
}

private def classDirectory(workspace: Workspace, wipe: Boolean): Path = {
val path = testFolder.resolve(workspace.name).resolve("out")
if (wipe) {
Directory(path).deleteRecursively()
Files.createDirectories(path)
}
path.toAbsolutePath
}

private def dependencyClasspath(workspace: Workspace): Seq[String] = {
BuildInfo.ideTestsDependencyClasspath.map(_.getAbsolutePath) ++
workspace.dependsOn.flatMap { dep =>
classDirectory(dep, wipe = false).toString +: dependencyClasspath(dep)
}
}.distinct

private def sourceDirectory(workspace: Workspace, wipe: Boolean): Path = {
val path = TestFile.sourceDir.resolve(workspace.name).toAbsolutePath
if (wipe) {
Directory(path).deleteRecursively()
Files.createDirectories(path)
}
path
}

/**
* Sets up the sources of the given workspace, creates the necessary directories
* and compile the sources.
*
* @param workspace The workspace to set up.
*/
private def compileWorkspace(workspace: Workspace): Unit = {
val sourcesDir = sourceDirectory(workspace, wipe = true)
val sources = workspace.sources.zipWithIndex.map { case (src, id) =>
val path = sourcesDir.resolve(src.sourceName(id)).toAbsolutePath
Files.write(path, src.text.getBytes("UTF-8"))
path.toString
}

val compileOptions =
sources.toArray ++
Array(
"-classpath", dependencyClasspath(workspace).mkString(pathSeparator),
"-d", classDirectory(workspace, wipe = true).toString
)
val reporter = new ThrowingReporter(Reporter.NoReporter)
Main.process(compileOptions, reporter)
}

}
Loading