Skip to content

Commit 812ba2a

Browse files
committed
Add tests for worksheets
1 parent 4c1c8e6 commit 812ba2a

File tree

8 files changed

+207
-25
lines changed

8 files changed

+207
-25
lines changed

language-server/src/dotty/tools/languageserver/Worksheet.scala

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package dotty.tools.languageserver
22

3-
import dotty.tools.dotc.ast.tpd.{Template, Tree, TypeDef}
3+
import dotty.tools.dotc.ast.tpd.{DefTree, Template, Tree, TypeDef}
44
import dotty.tools.dotc.core.Contexts.Context
55
import dotty.tools.dotc.interactive.SourceTree
6+
import dotty.tools.dotc.util.Positions.Position
67
import dotty.tools.dotc.util.SourceFile
78

9+
import dotty.tools.dotc.core.Flags.Synthetic
10+
811
import dotty.tools.repl.{ReplDriver, State}
912

1013
import java.io.{ByteArrayOutputStream, PrintStream}
@@ -22,14 +25,21 @@ object Worksheet {
2225
case td @ TypeDef(_, template: Template) =>
2326
val replOut = new ByteArrayOutputStream
2427
val repl = new ReplDriver(replOptions, out = new PrintStream(replOut))
28+
val executed = collection.mutable.Set.empty[(Int, Int)]
2529

2630
template.body.foldLeft(repl.initialState) {
27-
case (state, statement) =>
31+
case (state, statement: DefTree) if statement.symbol.is(Synthetic) =>
32+
state
33+
34+
case (state, statement) if executed.add(bounds(statement.pos)) =>
2835
val (line, newState) = execute(repl, state, statement, tree.source)
2936
val result = new String(replOut.toByteArray())
30-
sendMessage(encode(result, line))
37+
if (result.trim.nonEmpty) sendMessage(encode(result, line))
3138
replOut.reset()
3239
newState
40+
41+
case (state, statement) =>
42+
state
3343
}
3444
}
3545
}
@@ -54,4 +64,6 @@ object Worksheet {
5464

5565
private def encode(message: String, line: Int): String =
5666
line + ":" + message
67+
68+
private def bounds(pos: Position): (Int, Int) = (pos.start, pos.end)
5769
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package dotty.tools.languageserver
2+
3+
import org.junit.Test
4+
5+
import dotty.tools.languageserver.util.Code._
6+
7+
class WorksheetTest {
8+
9+
@Test def evaluateExpression: Unit = {
10+
ws"${m1}2 + 2".withSource
11+
.evaluate(m1, "1:val res0: Int = 4")
12+
}
13+
14+
@Test def evaluateSimpleVal: Unit = {
15+
ws"${m1}val foo = 123".withSource
16+
.evaluate(m1, "1:val foo: Int = 123")
17+
}
18+
19+
@Test def usePreviousDefinition: Unit = {
20+
ws"""${m1}val foo = 123
21+
val bar = foo + 1""".withSource
22+
.evaluate(m1, "1:val foo: Int = 123",
23+
"2:val bar: Int = 124")
24+
}
25+
26+
@Test def defineObject: Unit = {
27+
ws"""${m1}def foo(x: Int) = x + 1
28+
foo(1)""".withSource
29+
.evaluate(m1, "1:def foo(x: Int): Int",
30+
"2:val res0: Int = 2")
31+
}
32+
33+
@Test def defineCaseClass: Unit = {
34+
ws"""${m1} case class Foo(x: Int)
35+
Foo(1)""".withSource
36+
.evaluate(m1, "1:// defined case class Foo",
37+
"2:val res0: Foo = Foo(1)")
38+
}
39+
40+
@Test def defineClass: Unit = {
41+
ws"""${m1}class Foo(x: Int) {
42+
override def toString: String = "Foo"
43+
}
44+
new Foo(1)""".withSource
45+
.evaluate(m1, "3:// defined class Foo",
46+
"4:val res0: Foo = Foo")
47+
}
48+
49+
@Test def defineAnonymousClass0: Unit = {
50+
ws"""${m1}new {
51+
override def toString: String = "Foo"
52+
}""".withSource
53+
.evaluate(m1, "3:val res0: Object = Foo")
54+
}
55+
56+
@Test def defineAnonymousClass1: Unit = {
57+
ws"""${m1}class Foo
58+
trait Bar
59+
new Foo with Bar {
60+
override def toString: String = "Foo"
61+
}""".withSource
62+
.evaluate(m1, "1:// defined class Foo",
63+
"2:// defined trait Bar",
64+
"5:val res0: Foo & Bar = Foo")
65+
}
66+
67+
@Test def produceMultilineOutput: Unit = {
68+
ws"""${m1}1 to 3 foreach println""".withSource
69+
.evaluate(m1, "1:1\n2\n3")
70+
}
71+
72+
@Test def patternMatching0: Unit = {
73+
ws"""${m1}1 + 2 match {
74+
case x if x % 2 == 0 => "even"
75+
case _ => "odd"
76+
}""".withSource
77+
.evaluate(m1, "4:val res0: String = odd")
78+
}
79+
80+
@Test def patternMatching1: Unit = {
81+
ws"""${m1}val (foo, bar) = (1, 2)""".withSource
82+
.evaluate(m1, "1:val foo: Int = 1\nval bar: Int = 2")
83+
}
84+
85+
}

language-server/test/dotty/tools/languageserver/util/Code.scala

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,22 @@ object Code {
3535
* and `m3` and `m4` enclose the identifier `Hello`. These positions can then be used to ask to
3636
* perform actions such as finding all references, etc.
3737
*/
38-
def code(args: Embedded*): SourceWithPositions = {
38+
def code(args: Embedded*): ScalaSourceWithPositions = {
39+
val (text, positions) = textAndPositions(args: _*)
40+
ScalaSourceWithPositions(text, positions)
41+
}
42+
43+
/**
44+
* An interpolator similar to `code`, but used for defining a worksheet.
45+
*
46+
* @see code
47+
*/
48+
def ws(args: Embedded*): WorksheetWithPositions = {
49+
val (text, positions) = textAndPositions(args: _*)
50+
WorksheetWithPositions(text, positions)
51+
}
52+
53+
private def textAndPositions(args: Embedded*): (String, List[(CodeMarker, Int, Int)]) = {
3954
val pi = sc.parts.iterator
4055
val ai = args.iterator
4156

@@ -70,22 +85,40 @@ object Code {
7085
if (pi.hasNext)
7186
stringBuilder.append(pi.next())
7287

73-
SourceWithPositions(stringBuilder.result(), positions.result())
88+
(stringBuilder.result(), positions.result())
7489
}
7590
}
7691

7792
/** A new `CodeTester` working with `sources` in the workspace. */
7893
def withSources(sources: SourceWithPositions*): CodeTester = new CodeTester(sources.toList, Nil)
7994

95+
sealed trait SourceWithPositions {
96+
97+
/** The code contained within the virtual source file. */
98+
def text: String
99+
100+
/** The positions of the markers that have been set. */
101+
def positions: List[(CodeMarker, Int, Int)]
102+
103+
/** A new `CodeTester` with only this source in the workspace. */
104+
def withSource: CodeTester = new CodeTester(this :: Nil, Nil)
105+
106+
}
107+
80108
/**
81-
* A virtual source file where several markers have been set.
109+
* A virtual Scala source file where several markers have been set.
82110
*
83111
* @param text The code contained within the virtual source file.
84112
* @param positions The positions of the markers that have been set.
85113
*/
86-
case class SourceWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) {
87-
/** A new `CodeTester` with only this source in the workspace. */
88-
def withSource: CodeTester = new CodeTester(this :: Nil, Nil)
89-
}
114+
case class ScalaSourceWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions
115+
116+
/**
117+
* A virtual worksheet where several markers have been set.
118+
*
119+
* @param text The code contained within the virtual source file.
120+
* @param positions The positions of the markers that have been set.
121+
*/
122+
case class WorksheetWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions
90123

91124
}

language-server/test/dotty/tools/languageserver/util/CodeTester.scala

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package dotty.tools.languageserver.util
22

3-
import dotty.tools.languageserver.util.Code.SourceWithPositions
3+
import dotty.tools.languageserver.util.Code._
44
import dotty.tools.languageserver.util.actions._
55
import dotty.tools.languageserver.util.embedded.CodeMarker
66
import dotty.tools.languageserver.util.server.{TestFile, TestServer}
@@ -16,8 +16,9 @@ class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) {
1616

1717
private val testServer = new TestServer(TestFile.testDir)
1818

19-
private val files = sources.zipWithIndex.map { case (code, i) =>
20-
testServer.openCode(code.text, s"Source$i.scala")
19+
private val files = sources.zipWithIndex.map {
20+
case (ScalaSourceWithPositions(text, _), i) => testServer.openCode(text, s"Source$i.scala")
21+
case (WorksheetWithPositions(text, _), i) => testServer.openCode(text, s"Worksheet$i.sc")
2122
}
2223
private val positions: PositionContext = getPositions(files)
2324

@@ -116,9 +117,12 @@ class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) {
116117
def symbol(query: String, symbols: SymInfo*): this.type =
117118
doAction(new CodeSymbol(query, symbols))
118119

120+
def evaluate(marker: CodeMarker, expected: String*): this.type =
121+
doAction(new WorksheetEvaluate(marker, expected))
122+
119123
private def doAction(action: Action): this.type = {
120124
try {
121-
action.execute()(testServer, positions)
125+
action.execute()(testServer, testServer.client, positions)
122126
} catch {
123127
case ex: AssertionError =>
124128
val sourcesStr = sources.zip(files).map{ case (source, file) => "// " + file.file + "\n" + source.text}.mkString("\n")

language-server/test/dotty/tools/languageserver/util/actions/Action.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package dotty.tools.languageserver.util.actions
22

33
import dotty.tools.languageserver.DottyLanguageServer
44
import dotty.tools.languageserver.util.PositionContext
5-
import dotty.tools.languageserver.util.server.TestServer
5+
import dotty.tools.languageserver.util.server.{TestClient, TestServer}
66

77
import PositionContext._
88

@@ -11,7 +11,7 @@ import PositionContext._
1111
* definition, etc.)
1212
*/
1313
trait Action {
14-
type Exec[T] = implicit (TestServer, PositionContext) => T
14+
type Exec[T] = implicit (TestServer, TestClient, PositionContext) => T
1515

1616
/** Execute the action. */
1717
def execute(): Exec[Unit]
@@ -22,4 +22,7 @@ trait Action {
2222
/** The server that this action targets. */
2323
def server: Exec[DottyLanguageServer] = implicitly[TestServer].server
2424

25+
/** The client that executes this action. */
26+
def client: Exec[TestClient] = implicitly[TestClient]
27+
2528
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dotty.tools.languageserver.util.actions
2+
3+
import dotty.tools.languageserver.util.{PositionContext}
4+
import dotty.tools.languageserver.util.embedded.CodeMarker
5+
6+
import org.eclipse.lsp4j.{DidSaveTextDocumentParams, MessageParams, MessageType}
7+
8+
import org.junit.Assert.assertEquals
9+
10+
class WorksheetEvaluate(marker: CodeMarker, expected: Seq[String]) extends Action {
11+
override def execute(): Exec[Unit] = {
12+
val file = marker.toTextDocumentIdentifier
13+
server.didSave(new DidSaveTextDocumentParams(file))
14+
15+
while (!getLogs(marker).contains("FINISHED")) Thread.sleep(100)
16+
17+
assertEquals(expected, getLogs(marker).init)
18+
client.log.clear()
19+
}
20+
21+
override def show: PositionContext.PosCtx[String] =
22+
s"WorksheetEvaluate(${marker.file}, ${expected})"
23+
24+
private def getLogs(marker: CodeMarker): Exec[List[String]] = {
25+
def matches(params: MessageParams): Boolean =
26+
params.getType == MessageType.Info && params.getMessage.startsWith(marker.file.uri)
27+
client.log.get.collect {
28+
case params: MessageParams if matches(params) =>
29+
params.getMessage.substring(marker.file.uri.length).trim
30+
}
31+
}
32+
}

language-server/test/dotty/tools/languageserver/util/server/TestClient.scala

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,42 @@ import java.util.concurrent.CompletableFuture
55
import org.eclipse.lsp4j._
66
import org.eclipse.lsp4j.services._
77

8+
import scala.collection.mutable.Buffer
9+
810
class TestClient extends LanguageClient {
911

10-
private val log = new StringBuilder
12+
class Log[T] {
13+
private[this] val log = Buffer.empty[T]
14+
15+
def +=(elem: T): this.type = { log += elem; this }
16+
def get: List[T] = log.toList
17+
def clear(): Unit = log.clear()
18+
}
19+
20+
val log = new Log[MessageParams]
21+
val diagnostics = new Log[PublishDiagnosticsParams]
22+
val telemetry = new Log[Any]
1123

12-
def getLog: String = log.result()
1324

1425
override def logMessage(message: MessageParams) = {
15-
log.append(message.toString)
26+
log += message
1627
}
1728

1829
override def showMessage(messageParams: MessageParams) = {
19-
log.append(messageParams.toString)
30+
log += messageParams
2031
}
2132

2233
override def telemetryEvent(obj: scala.Any) = {
23-
log.append(obj.toString)
34+
telemetry += obj
2435
}
2536

2637
override def showMessageRequest(requestParams: ShowMessageRequestParams) = {
27-
log.append(requestParams.toString)
38+
log += requestParams
2839
new CompletableFuture[MessageActionItem]
2940
}
3041

31-
override def publishDiagnostics(diagnostics: PublishDiagnosticsParams) = {
32-
log.append(diagnostics.toString)
42+
override def publishDiagnostics(diagnosticsParams: PublishDiagnosticsParams) = {
43+
diagnostics += diagnosticsParams
3344
}
3445

3546
}

language-server/test/dotty/tools/languageserver/util/server/TestServer.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import org.eclipse.lsp4j.{ DidOpenTextDocumentParams, InitializeParams, Initiali
1111
class TestServer(testFolder: Path) {
1212

1313
val server = new DottyLanguageServer
14+
var client: TestClient = _
15+
1416
init()
1517

1618
private[this] def init(): InitializeResult = {
@@ -36,7 +38,7 @@ class TestServer(testFolder: Path) {
3638
close()
3739
}
3840

39-
val client = new TestClient
41+
client = new TestClient
4042
server.connect(client)
4143

4244
val initParams = new InitializeParams()

0 commit comments

Comments
 (0)