diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index 67d3827145f4..44124147e395 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -115,6 +115,7 @@ transport formats, please look [here](./protocol-architecture). - [`executionContext/expressionUpdates`](#executioncontextexpressionupdates) - [`executionContext/executionFailed`](#executioncontextexecutionfailed) - [`executionContext/executionStatus`](#executioncontextexecutionstatus) + - [`executionContext/executeExpression`](#executioncontextexecuteexpression) - [`executionContext/attachVisualisation`](#executioncontextattachvisualisation) - [`executionContext/detachVisualisation`](#executioncontextdetachvisualisation) - [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation) @@ -1280,6 +1281,7 @@ destroying the context. - [`executionContext/recompute`](#executioncontextrecompute) - [`executionContext/push`](#executioncontextpush) - [`executionContext/pop`](#executioncontextpop) +- [`executionContext/executeExpression`](#executioncontextexecuteexpression) - [`executionContext/attachVisualisation`](#executioncontextattachvisualisation) - [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation) - [`executionContext/detachVisualisation`](#executioncontextdetachvisualisation) @@ -2751,6 +2753,47 @@ Sent from the server to the client to inform about a status of execution. None +### `executionContext/executeExpression` + +This message allows the client to execute an arbitrary expression on a given +node. It behaves like oneshot +[`executionContext/attachVisualisation`](#executioncontextattachvisualisation) +visualisation request, meaning that the visualisation expression will be +executed only once. + +- **Type:** Request +- **Direction:** Client -> Server +- **Connection:** Protocol +- **Visibility:** Public + +#### Parameters + +```typescript +interface ExecuteExpressionRequest { + visualisationId: UUID; + expressionId: UUID; + visualisationConfig: VisualisationConfiguration; +} +``` + +#### Result + +```typescript +null; +``` + +#### Errors + +- [`AccessDeniedError`](#accessdeniederror) when the user does not hold the + `executionContext/canModify` capability for this context. +- [`ContextNotFoundError`](#contextnotfounderror) when context can not be found + by provided id. +- [`ModuleNotFoundError`](#modulenotfounderror) to signal that the module with + the visualisation cannot be found. +- [`VisualisationExpressionError`](#visualisationexpressionerror) to signal that + the expression specified in the `VisualisationConfiguration` cannot be + evaluated. + ### `executionContext/attachVisualisation` This message allows the client to attach a visualisation, potentially diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala index eb7f6faa73ae..d32931ef0d71 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala @@ -38,6 +38,7 @@ import org.enso.languageserver.requesthandler.text._ import org.enso.languageserver.requesthandler.visualisation.{ AttachVisualisationHandler, DetachVisualisationHandler, + ExecuteExpressionHandler, ModifyVisualisationHandler } import org.enso.languageserver.runtime.ContextRegistryProtocol @@ -45,6 +46,7 @@ import org.enso.languageserver.runtime.ExecutionApi._ import org.enso.languageserver.runtime.VisualisationApi.{ AttachVisualisation, DetachVisualisation, + ExecuteExpression, ModifyVisualisation } import org.enso.languageserver.search.SearchApi._ @@ -94,7 +96,7 @@ class JsonConnectionController( import context.dispatcher - implicit val timeout = Timeout(requestTimeout) + implicit val timeout: Timeout = Timeout(requestTimeout) override def receive: Receive = { case JsonRpcServer.WebConnect(webActor) => @@ -333,6 +335,8 @@ class JsonConnectionController( Completion -> search.CompletionHandler .props(requestTimeout, suggestionsHandler), Import -> search.ImportHandler.props(requestTimeout, suggestionsHandler), + ExecuteExpression -> ExecuteExpressionHandler + .props(rpcSession.clientId, requestTimeout, contextRegistry), AttachVisualisation -> AttachVisualisationHandler .props(rpcSession.clientId, requestTimeout, contextRegistry), DetachVisualisation -> DetachVisualisationHandler diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala index 3ef78769fbcb..52f3d87dd8ab 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala @@ -52,6 +52,7 @@ object JsonRpc { .registerRequest(ExecutionContextPush) .registerRequest(ExecutionContextPop) .registerRequest(ExecutionContextRecompute) + .registerRequest(ExecuteExpression) .registerRequest(AttachVisualisation) .registerRequest(DetachVisualisation) .registerRequest(ModifyVisualisation) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteExpressionHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteExpressionHandler.scala new file mode 100644 index 000000000000..c9c94ac0ae69 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteExpressionHandler.scala @@ -0,0 +1,90 @@ +package org.enso.languageserver.requesthandler.visualisation + +import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props} +import org.enso.jsonrpc._ +import org.enso.languageserver.data.ClientId +import org.enso.languageserver.requesthandler.RequestTimeout +import org.enso.languageserver.runtime.VisualisationApi.ExecuteExpression +import org.enso.languageserver.runtime.{ + ContextRegistryProtocol, + RuntimeFailureMapper +} +import org.enso.languageserver.util.UnhandledLogging + +import scala.concurrent.duration.FiniteDuration + +/** A request handler for `executionContext/executeExpression` command. + * + * @param clientId an unique identifier of the client + * @param timeout request timeout + * @param contextRegistry a reference to the context registry. + */ +class ExecuteExpressionHandler( + clientId: ClientId, + timeout: FiniteDuration, + contextRegistry: ActorRef +) extends Actor + with ActorLogging + with UnhandledLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request( + ExecuteExpression, + id, + params: ExecuteExpression.Params + ) => + contextRegistry ! ContextRegistryProtocol.ExecuteExpression( + clientId, + params.visualisationId, + params.expressionId, + params.visualisationConfig + ) + val cancellable = + context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) + context.become(responseStage(id, sender(), cancellable)) + } + + private def responseStage( + id: Id, + replyTo: ActorRef, + cancellable: Cancellable + ): Receive = { + case RequestTimeout => + log.error("Request [{}] timed out.", id) + replyTo ! ResponseError(Some(id), Errors.RequestTimeout) + context.stop(self) + + case ContextRegistryProtocol.VisualisationAttached => + replyTo ! ResponseResult(ExecuteExpression, id, Unused) + cancellable.cancel() + context.stop(self) + + case error: ContextRegistryProtocol.Failure => + replyTo ! ResponseError(Some(id), RuntimeFailureMapper.mapFailure(error)) + cancellable.cancel() + context.stop(self) + } + +} + +object ExecuteExpressionHandler { + + /** Creates configuration object used to create an + * [[ExecuteExpressionHandler]]. + * + * @param clientId an unique identifier of the client + * @param timeout request timeout + * @param runtime a reference to the context registry + */ + def props( + clientId: ClientId, + timeout: FiniteDuration, + runtime: ActorRef + ): Props = + Props(new ExecuteExpressionHandler(clientId, timeout, runtime)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala index fe1a30915127..49689a7f4dc2 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala @@ -4,6 +4,8 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props} import akka.pattern.pipe import org.enso.languageserver.data.Config import org.enso.languageserver.runtime.ContextRegistryProtocol.{ + DetachVisualisation, + RegisterOneshotVisualisation, VisualisationContext, VisualisationUpdate } @@ -60,9 +62,31 @@ final class ContextEventsListener( } } - override def receive: Receive = withState(Vector()) + override def receive: Receive = withState(Set(), Vector()) + + def withState( + oneshotVisualisations: Set[Api.VisualisationContext], + expressionUpdates: Vector[Api.ExpressionUpdate] + ): Receive = { + + case RegisterOneshotVisualisation( + contextId, + visualisationId, + expressionId + ) => + val visualisationContext = + Api.VisualisationContext( + visualisationId, + contextId, + expressionId + ) + context.become( + withState( + oneshotVisualisations + visualisationContext, + expressionUpdates + ) + ) - def withState(expressionUpdates: Vector[Api.ExpressionUpdate]): Receive = { case Api.VisualisationUpdate(ctx, data) if ctx.contextId == contextId => val payload = VisualisationUpdate( @@ -74,10 +98,24 @@ final class ContextEventsListener( data ) sessionRouter ! DeliverToBinaryController(rpcSession.clientId, payload) + if (oneshotVisualisations.contains(ctx)) { + context.parent ! DetachVisualisation( + rpcSession.clientId, + contextId, + ctx.visualisationId, + ctx.expressionId + ) + context.become( + withState( + oneshotVisualisations - ctx, + expressionUpdates + ) + ) + } case Api.ExpressionUpdates(`contextId`, apiUpdates) => context.become( - withState(expressionUpdates :++ apiUpdates) + withState(oneshotVisualisations, expressionUpdates :++ apiUpdates) ) case Api.ExecutionFailed(`contextId`, error) => @@ -115,7 +153,7 @@ final class ContextEventsListener( case RunExpressionUpdates if expressionUpdates.nonEmpty => runExpressionUpdates(expressionUpdates) - context.become(withState(Vector())) + context.become(withState(oneshotVisualisations, Vector())) case RunExpressionUpdates if expressionUpdates.isEmpty => } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala index dc9f72d76032..8f104b15b05e 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala @@ -182,20 +182,42 @@ final class ContextRegistry( sender() ! AccessDenied } + case ExecuteExpression(clientId, visualisationId, expressionId, cfg) => + val contextId = cfg.executionContextId + if (store.hasContext(clientId, contextId)) { + store.getListener(contextId).foreach { listener => + listener ! RegisterOneshotVisualisation( + contextId, + visualisationId, + expressionId + ) + } + val handler = + context.actorOf( + AttachVisualisationHandler.props(config, timeout, runtime) + ) + handler.forward( + Api.AttachVisualisation( + visualisationId, + expressionId, + convertVisualisationConfig(cfg) + ) + ) + } else { + sender() ! AccessDenied + } + case AttachVisualisation(clientId, visualisationId, expressionId, cfg) => if (store.hasContext(clientId, cfg.executionContextId)) { val handler = context.actorOf( AttachVisualisationHandler.props(config, timeout, runtime) ) - - val configuration = convertVisualisationConfig(cfg) - handler.forward( Api.AttachVisualisation( visualisationId, expressionId, - configuration + convertVisualisationConfig(cfg) ) ) } else { @@ -213,7 +235,6 @@ final class ContextRegistry( context.actorOf( DetachVisualisationHandler.props(config, timeout, runtime) ) - handler.forward( Api.DetachVisualisation(contextId, visualisationId, expressionId) ) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala index e62dc05dd270..89cbdc3806b3 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala @@ -307,6 +307,45 @@ object ContextRegistryProtocol { diagnostics: Seq[ExecutionDiagnostic] ) + /** Requests the language server to execute an expression provided in the + * `visualisationConfig` on an expression specified by `expressionId`. + * + * @param clientId the requester id + * @param visualisationId an identifier of a visualisation + * @param expressionId an identifier of an expression which is visualised + * @param visualisationConfig a configuration object for properties of the + * visualisation + */ + case class ExecuteExpression( + clientId: ClientId, + visualisationId: UUID, + expressionId: UUID, + visualisationConfig: VisualisationConfiguration + ) extends ToLogString { + + /** @inheritdoc */ + override def toLogString(shouldMask: Boolean): String = + "ExecuteExpression(" + + s"clientId=$clientId," + + s"visualisationId=$visualisationId," + + s"expressionId=$expressionId,visualisationConfig=" + + visualisationConfig.toLogString(shouldMask) + + ")" + } + + /** Registers a oneshot visualisation that will be detached after the first + * execution. + * + * @param contextId execution context identifier + * @param visualisationId an identifier of a visualisation + * @param expressionId an identifier of an expression which is visualised + */ + case class RegisterOneshotVisualisation( + contextId: ContextId, + visualisationId: UUID, + expressionId: UUID + ) + /** Requests the language server to attach a visualisation to the expression * specified by `expressionId`. * @@ -314,7 +353,7 @@ object ContextRegistryProtocol { * @param visualisationId an identifier of a visualisation * @param expressionId an identifier of an expression which is visualised * @param visualisationConfig a configuration object for properties of the - * visualisation + * visualisation */ case class AttachVisualisation( clientId: ClientId, @@ -333,8 +372,7 @@ object ContextRegistryProtocol { ")" } - /** Signals that attaching a visualisation has succeeded. - */ + /** Signals that attaching a visualisation has succeeded. */ case object VisualisationAttached /** Requests the language server to detach a visualisation from the expression diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala index 593288a92f52..3b680a3fb3b4 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala @@ -10,6 +10,23 @@ import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused} */ object VisualisationApi { + case object ExecuteExpression + extends Method("executionContext/executeExpression") { + + case class Params( + visualisationId: UUID, + expressionId: UUID, + visualisationConfig: VisualisationConfiguration + ) + + implicit val hasParams = new HasParams[this.type] { + type Params = ExecuteExpression.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = Unused.type + } + } + case object AttachVisualisation extends Method("executionContext/attachVisualisation") { diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/runtime/ContextEventsListenerSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/ContextEventsListenerSpec.scala index 76852304e5a8..6f27d8e763d1 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/runtime/ContextEventsListenerSpec.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/ContextEventsListenerSpec.scala @@ -3,6 +3,7 @@ package org.enso.languageserver.runtime import java.io.File import java.nio.file.Files import java.util.UUID + import akka.actor.{ActorRef, ActorSystem} import akka.testkit.{ImplicitSender, TestKit, TestProbe} import org.apache.commons.io.FileUtils @@ -15,12 +16,14 @@ import org.enso.languageserver.data.{ } import org.enso.languageserver.event.InitializedEvent import org.enso.languageserver.runtime.ContextRegistryProtocol.{ + DetachVisualisation, ExecutionDiagnostic, ExecutionDiagnosticKind, ExecutionDiagnosticNotification, ExecutionFailedNotification, ExecutionFailure, ExpressionUpdatesNotification, + RegisterOneshotVisualisation, VisualisationContext, VisualisationEvaluationFailed, VisualisationUpdate @@ -61,12 +64,13 @@ class ContextEventsListenerSpec "ContextEventsListener" should { - "not send empty updates" taggedAs Retry in withDb { (_, _, _, router, _) => - router.expectNoMessage() + "not send empty updates" taggedAs Retry in withDb { + (_, _, _, router, _, _) => + router.expectNoMessage() } "send expression updates" taggedAs Retry in withDb { - (clientId, contextId, repo, router, listener) => + (clientId, contextId, repo, router, _, listener) => val (_, suggestionIds) = Await.result( repo.insertAll( Seq( @@ -120,7 +124,7 @@ class ContextEventsListenerSpec } "send dataflow error updates" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => listener ! Api.ExpressionUpdates( contextId, Set( @@ -159,7 +163,7 @@ class ContextEventsListenerSpec } "send runtime error updates" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => listener ! Api.ExpressionUpdates( contextId, Set( @@ -196,7 +200,7 @@ class ContextEventsListenerSpec } "send expression updates grouped" taggedAs Retry in withDb(0.seconds) { - (clientId, contextId, repo, router, listener) => + (clientId, contextId, repo, router, _, listener) => Await.result( repo.insertAll( Seq( @@ -267,16 +271,89 @@ class ContextEventsListenerSpec ) } + "register oneshot visualization" taggedAs Retry in withDb { + (clientId, contextId, _, router, registry, listener) => + val ctx = Api.VisualisationContext( + UUID.randomUUID(), + contextId, + UUID.randomUUID() + ) + + listener ! RegisterOneshotVisualisation( + ctx.contextId, + ctx.visualisationId, + ctx.expressionId + ) + + val data1 = Array[Byte](1, 2, 3) + listener ! Api.VisualisationUpdate(ctx, data1) + router.expectMsg( + DeliverToBinaryController( + clientId, + VisualisationUpdate( + VisualisationContext( + ctx.visualisationId, + ctx.contextId, + ctx.expressionId + ), + data1 + ) + ) + ) + registry.expectMsg( + DetachVisualisation( + clientId, + ctx.contextId, + ctx.visualisationId, + ctx.expressionId + ) + ) + + val data2 = Array[Byte](2, 3, 4) + listener ! Api.VisualisationUpdate(ctx, data2) + router.expectMsg( + DeliverToBinaryController( + clientId, + VisualisationUpdate( + VisualisationContext( + ctx.visualisationId, + ctx.contextId, + ctx.expressionId + ), + data2 + ) + ) + ) + registry.expectNoMessage() + } + "send visualization updates" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, registry, listener) => val ctx = Api.VisualisationContext( UUID.randomUUID(), contextId, UUID.randomUUID() ) - val data = Array[Byte](1, 2, 3) - listener ! Api.VisualisationUpdate(ctx, data) + val data1 = Array[Byte](1, 2, 3) + listener ! Api.VisualisationUpdate(ctx, data1) + router.expectMsg( + DeliverToBinaryController( + clientId, + VisualisationUpdate( + VisualisationContext( + ctx.visualisationId, + ctx.contextId, + ctx.expressionId + ), + data1 + ) + ) + ) + registry.expectNoMessage() + + val data2 = Array[Byte](2, 3, 4) + listener ! Api.VisualisationUpdate(ctx, data2) router.expectMsg( DeliverToBinaryController( clientId, @@ -286,14 +363,15 @@ class ContextEventsListenerSpec ctx.contextId, ctx.expressionId ), - data + data2 ) ) ) + registry.expectNoMessage() } "send execution failed notification" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => val message = "Test execution failed" listener ! Api.ExecutionFailed( contextId, @@ -315,7 +393,7 @@ class ContextEventsListenerSpec } "send execution update notification" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => val message = "Test execution failed" listener ! Api.ExecutionUpdate( contextId, @@ -343,7 +421,7 @@ class ContextEventsListenerSpec } "send visualisation evaluation failed notification" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => val message = "Test visualisation evaluation failed" val visualisationId = UUID.randomUUID() val expressionId = UUID.randomUUID() @@ -384,21 +462,36 @@ class ContextEventsListenerSpec JsonSession(clientId, TestProbe().ref) def withDb( - test: (UUID, UUID, SuggestionsRepo[Future], TestProbe, ActorRef) => Any + test: ( + UUID, + UUID, + SuggestionsRepo[Future], + TestProbe, + TestProbe, + ActorRef + ) => Any ): Unit = withDb(100.millis)(test) def withDb(updatesSendRate: FiniteDuration)( - test: (UUID, UUID, SuggestionsRepo[Future], TestProbe, ActorRef) => Any + test: ( + UUID, + UUID, + SuggestionsRepo[Future], + TestProbe, + TestProbe, + ActorRef + ) => Any ): Unit = { val testContentRoot = Files.createTempDirectory(null).toRealPath() sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile)) - val config = newConfig(testContentRoot.toFile) - val clientId = UUID.randomUUID() - val contextId = UUID.randomUUID() - val router = TestProbe("session-router") - val repo = SqlSuggestionsRepo(config.directories.suggestionsDatabaseFile) - val listener = system.actorOf( + val config = newConfig(testContentRoot.toFile) + val clientId = UUID.randomUUID() + val contextId = UUID.randomUUID() + val router = TestProbe("session-router") + val contextRegistry = TestProbe("context-registry") + val repo = SqlSuggestionsRepo(config.directories.suggestionsDatabaseFile) + val listener = contextRegistry.childActorOf( ContextEventsListener.props( config, repo, @@ -419,7 +512,7 @@ class ContextEventsListenerSpec } Await.ready(repoInit, Timeout) - try test(clientId, contextId, repo, router, listener) + try test(clientId, contextId, repo, router, contextRegistry, listener) finally { system.stop(listener) repo.close() diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala index 754d48382628..3592571aa01e 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala @@ -453,6 +453,115 @@ class ContextRegistryTest extends BaseServerTest { client.expectJson(json.ok(3)) } + "successfully execute expression" in { + val client = getInitialisedWsClient() + + // create context + client.send(json.executionContextCreateRequest(1)) + val (requestId, contextId) = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request(requestId, Api.CreateContextRequest(contextId)) => + (requestId, contextId) + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId, + Api.CreateContextResponse(contextId) + ) + client.expectJson(json.executionContextCreateResponse(1, contextId)) + + // attach visualisation + val visualisationId = UUID.randomUUID() + val expressionId = UUID.randomUUID() + val config = + VisualisationConfiguration(contextId, "Test.Main", ".to_json.to_text") + client.send( + json.executionContextExecuteExpressionRequest( + 2, + visualisationId, + expressionId, + config + ) + ) + val requestId2 = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request( + requestId, + Api.AttachVisualisation( + `visualisationId`, + `expressionId`, + _ + ) + ) => + requestId + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId2, + Api.VisualisationAttached() + ) + client.expectJson(json.ok(2)) + } + + "return ModuleNotFound error when executing expression" in { + val client = getInitialisedWsClient() + + // create context + client.send(json.executionContextCreateRequest(1)) + val (requestId, contextId) = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request(requestId, Api.CreateContextRequest(contextId)) => + (requestId, contextId) + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId, + Api.CreateContextResponse(contextId) + ) + client.expectJson(json.executionContextCreateResponse(1, contextId)) + + // attach visualisation + val visualisationId = UUID.randomUUID() + val expressionId = UUID.randomUUID() + val config = + VisualisationConfiguration(contextId, "Test.Main", ".to_json.to_text") + client.send( + json.executionContextExecuteExpressionRequest( + 2, + visualisationId, + expressionId, + config + ) + ) + val requestId2 = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request( + requestId, + Api.AttachVisualisation( + `visualisationId`, + `expressionId`, + _ + ) + ) => + requestId + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId2, + Api.ModuleNotFound(config.visualisationModule) + ) + client.expectJson( + json.executionContextModuleNotFound( + 2, + config.visualisationModule + ) + ) + } + "successfully attach visualisation" in { val client = getInitialisedWsClient() diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala index 76e9518825d5..724ccbc6838b 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala @@ -93,6 +93,28 @@ object ExecutionContextJsonMessages { } """ + def executionContextExecuteExpressionRequest( + reqId: Int, + visualisationId: Api.VisualisationId, + expressionId: Api.ExpressionId, + configuration: VisualisationConfiguration + ) = + json""" + { "jsonrpc": "2.0", + "method": "executionContext/executeExpression", + "id": $reqId, + "params": { + "visualisationId": $visualisationId, + "expressionId": $expressionId, + "visualisationConfig": { + "executionContextId": ${configuration.executionContextId}, + "visualisationModule": ${configuration.visualisationModule}, + "expression": ${configuration.expression} + } + } + } + """ + def executionContextAttachVisualisationRequest( reqId: Int, visualisationId: Api.VisualisationId,