From c9e5e4efe4b64dfc58335d14332c744ad45803d4 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Mon, 26 Apr 2021 13:15:04 +0300 Subject: [PATCH] Synchronize Runtime Updates Sending (#1691) Add `UpdatesSynchronizationState` that manages the runtime update messages and tracks which updates are sent to the user --- .../instrument/IdExecutionInstrument.java | 40 +- .../interpreter/instrument/RuntimeCache.java | 14 +- .../UpdatesSynchronizationState.java | 174 ++ .../interpreter/service/ExecutionService.java | 7 + .../instrument/CacheInvalidation.scala | 34 +- .../instrument/ExecutionContextState.scala | 9 +- .../instrument/command/EditFileCmd.scala | 6 +- .../instrument/command/PopContextCmd.scala | 20 +- .../instrument/command/PushContextCmd.scala | 13 +- .../command/RecomputeContextCmd.scala | 13 +- .../instrument/command/RenameProjectCmd.scala | 4 +- .../instrument/execution/Executable.scala | 7 +- .../instrument/job/EnsureCompiledJob.scala | 2 +- .../instrument/job/ExecuteJob.scala | 22 +- .../job/ProgramExecutionSupport.scala | 243 +-- .../job/UpsertVisualisationJob.scala | 22 +- .../test/instrument/RuntimeServerTest.scala | 1093 ------------ .../RuntimeVisualisationsTest.scala | 1583 +++++++++++++++++ 18 files changed, 2017 insertions(+), 1289 deletions(-) create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/instrument/UpdatesSynchronizationState.java create mode 100644 engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeVisualisationsTest.scala diff --git a/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java b/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java index a8d8c63a3239..f029fde0a2d3 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java @@ -192,6 +192,16 @@ public ProfilingInfo[] getProfilingInfo() { public boolean wasCached() { return wasCached; } + + /** @return {@code true} when the type differs from the cached value. */ + public boolean isTypeChanged() { + return !Objects.equals(type, cachedType); + } + + /** @return {@code true} when the function call differs from the cached value. */ + public boolean isFunctionCallChanged() { + return !Objects.equals(callInfo, cachedCallInfo); + } } /** Information about the function call. */ @@ -273,6 +283,7 @@ private static class IdExecutionEventListener implements ExecutionEventListener private final Consumer onExceptionalCallback; private final RuntimeCache cache; private final MethodCallsCache callsCache; + private final UpdatesSynchronizationState syncState; private final UUID nextExecutionItem; private final Map calls = new HashMap<>(); private final Timer timer; @@ -284,6 +295,7 @@ private static class IdExecutionEventListener implements ExecutionEventListener * @param entryCallTarget the call target being observed. * @param cache the precomputed expression values. * @param methodCallsCache the storage tracking the executed method calls. + * @param syncState the synchronization state of runtime updates. * @param nextExecutionItem the next item scheduled for execution. * @param functionCallCallback the consumer of function call events. * @param onComputedCallback the consumer of the computed value events. @@ -295,6 +307,7 @@ public IdExecutionEventListener( CallTarget entryCallTarget, RuntimeCache cache, MethodCallsCache methodCallsCache, + UpdatesSynchronizationState syncState, UUID nextExecutionItem, // The expression ID Consumer functionCallCallback, Consumer onComputedCallback, @@ -304,6 +317,7 @@ public IdExecutionEventListener( this.entryCallTarget = entryCallTarget; this.cache = cache; this.callsCache = methodCallsCache; + this.syncState = syncState; this.nextExecutionItem = nextExecutionItem; this.functionCallCallback = functionCallCallback; this.onComputedCallback = onComputedCallback; @@ -382,20 +396,29 @@ public void onReturnValue(EventContext context, VirtualFrame frame, Object resul UUID nodeId = ((ExpressionNode) node).getId(); String resultType = Types.getName(result); + String cachedType = cache.getType(nodeId); + FunctionCallInfo call = calls.get(nodeId); + FunctionCallInfo cachedCall = cache.getCall(nodeId); + ProfilingInfo[] profilingInfo = new ProfilingInfo[] {new ExecutionTime(nanoTimeElapsed)}; + + ExpressionValue expressionValue = + new ExpressionValue( + nodeId, result, resultType, cachedType, call, cachedCall, profilingInfo, false); + if (expressionValue.isTypeChanged() || expressionValue.isFunctionCallChanged()) { + syncState.setExpressionUnsync(nodeId); + } + syncState.setVisualisationUnsync(nodeId); + // Panics are not cached because a panic can be fixed by changing seemingly unrelated code, // like imports, and the invalidation mechanism can not always track those changes and // appropriately invalidate all dependent expressions. if (!isPanic) { cache.offer(nodeId, result); } - String cachedType = cache.putType(nodeId, resultType); - FunctionCallInfo call = calls.get(nodeId); - FunctionCallInfo cachedCall = cache.putCall(nodeId, call); - ProfilingInfo[] profilingInfo = new ProfilingInfo[] {new ExecutionTime(nanoTimeElapsed)}; + cache.putType(nodeId, resultType); + cache.putCall(nodeId, call); - onComputedCallback.accept( - new ExpressionValue( - nodeId, result, resultType, cachedType, call, cachedCall, profilingInfo, false)); + onComputedCallback.accept(expressionValue); if (isPanic) { throw context.createUnwind(result); } @@ -474,6 +497,7 @@ private UUID getNodeId(Node node) { * @param locationFilter the location filter. * @param cache the precomputed expression values. * @param methodCallsCache the storage tracking the executed method calls. + * @param syncState the synchronization state of runtime updates. * @param nextExecutionItem the next item scheduled for execution. * @param functionCallCallback the consumer of function call events. * @param onComputedCallback the consumer of the computed value events. @@ -486,6 +510,7 @@ public EventBinding bind( LocationFilter locationFilter, RuntimeCache cache, MethodCallsCache methodCallsCache, + UpdatesSynchronizationState syncState, UUID nextExecutionItem, Consumer functionCallCallback, Consumer onComputedCallback, @@ -505,6 +530,7 @@ public EventBinding bind( entryCallTarget, cache, methodCallsCache, + syncState, nextExecutionItem, functionCallCallback, onComputedCallback, diff --git a/engine/runtime/src/main/java/org/enso/interpreter/instrument/RuntimeCache.java b/engine/runtime/src/main/java/org/enso/interpreter/instrument/RuntimeCache.java index 25fc95a21f9e..389b19af7e6b 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/instrument/RuntimeCache.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/instrument/RuntimeCache.java @@ -1,7 +1,10 @@ package org.enso.interpreter.instrument; import java.lang.ref.SoftReference; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; /** A storage for computed values. */ public final class RuntimeCache { @@ -88,6 +91,15 @@ public Set getCalls() { return calls.keySet(); } + /** + * Remove the function call from the cache. + * + * @param key the expression associated with the function call. + */ + public void removeCall(UUID key) { + calls.remove(key); + } + /** Clear the cached calls. */ public void clearCalls() { calls.clear(); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/instrument/UpdatesSynchronizationState.java b/engine/runtime/src/main/java/org/enso/interpreter/instrument/UpdatesSynchronizationState.java new file mode 100644 index 000000000000..4fa75e0abe28 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/instrument/UpdatesSynchronizationState.java @@ -0,0 +1,174 @@ +package org.enso.interpreter.instrument; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * The synchronization state of runtime updates. + * + *

The thread executing the program can be interrupted at any moment. For example, the interrupt + * may happen when an expression is computed and the runtime state is changed, but before the update + * is sent to the user. And since the runtime state has changed, the server won't send the updates + * during the next execution. This class is supposed to address this issue keeping in sync the + * runtime state and the update messages. + * + *

Implementation + * + *

When implementing the synchronization, keep in mind the following principles: + * + *

    + *
  • Remove the synchronization flag before changing the server state. + *
  • Set the synchronization flag after the update is sent to the user. + *
+ * + * This way the server is guaranteed to send the update message at least once, regardless of when + * the thread interrupt has occurred. + * + *

The state consists of the following components: + * + *

    + *
  • Expressions state. Tracks all message updates that are sent when the expression metadata + * (e.g. the type or the underlying method pointer) is changed. + *
  • Method pointers state. Tracks message updates containing method pointers. Messages with + * method pointers are tracked separately from the expressions state because they have + * different invalidation rules. E.g., they should always be re-sent when the execution item + * is popped from the stack. + *
  • Visualisations state. Tracks the state of visualisation updates. + *
+ */ +public class UpdatesSynchronizationState { + + private final Set expressionsState = new HashSet<>(); + private final Set visualisationsState = new HashSet<>(); + private final Set methodPointersState = new HashSet<>(); + + @Override + public String toString() { + return "UpdatesSynchronizationState{" + + "expressionsState=" + + expressionsState + + ", visualisationsState=" + + visualisationsState + + ", methodPointersState=" + + methodPointersState + + '}'; + } + + /** + * Invalidate the state of the given expression. + * + * @param key the expression id. + */ + public void invalidate(UUID key) { + synchronized (this) { + expressionsState.remove(key); + visualisationsState.remove(key); + methodPointersState.remove(key); + } + } + + /* Expressions */ + + /** + * Checks if the given expression update is synchronized. + * + * @param key the expression id. + * @return {@code true} if the expression update is synchronized. + */ + public boolean isExpressionSync(UUID key) { + synchronized (expressionsState) { + return expressionsState.contains(key); + } + } + + /** + * Marks the given expression update as unsynchronized. + * + * @param key the expression id. + */ + public void setExpressionUnsync(UUID key) { + synchronized (expressionsState) { + expressionsState.remove(key); + } + } + + /** + * Marks the given expression update as synchronized. + * + * @param key the expression id. + */ + public void setExpressionSync(UUID key) { + synchronized (expressionsState) { + expressionsState.add(key); + } + } + + /* Visualisations */ + + /** + * Checks if the given visualisation update is synchronized. + * + * @param key the expression id. + * @return {@code true} if the visualisation update is synchronized. + */ + public boolean isVisualisationSync(UUID key) { + synchronized (visualisationsState) { + return visualisationsState.contains(key); + } + } + + /** + * Marks the given visualisation update as unsynchronized. + * + * @param key the expression id. + */ + public void setVisualisationUnsync(UUID key) { + synchronized (visualisationsState) { + visualisationsState.remove(key); + } + } + + /** + * Marks the given visualisation update as synchronized. + * + * @param key the expression id. + */ + public void setVisualisationSync(UUID key) { + synchronized (visualisationsState) { + visualisationsState.add(key); + } + } + + /* Method pointers */ + + /** + * Checks if the given method pointer is synchronized. + * + * @param key the expression id. + * @return {@code true} if the method pointer update is synchronized. + */ + public boolean isMethodPointerSync(UUID key) { + synchronized (methodPointersState) { + return methodPointersState.contains(key); + } + } + + /** + * Marks the method pointer as synchronized. + * + * @param key the expression id. + */ + public void setMethodPointerSync(UUID key) { + synchronized (methodPointersState) { + methodPointersState.add(key); + } + } + + /** Clears the synchronization state of all method pointers. */ + public void clearMethodPointersState() { + synchronized (methodPointersState) { + methodPointersState.clear(); + } + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java b/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java index e41ffec5ca7e..26d9fa96f832 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java @@ -10,6 +10,7 @@ import org.enso.interpreter.instrument.IdExecutionInstrument; import org.enso.interpreter.instrument.MethodCallsCache; import org.enso.interpreter.instrument.RuntimeCache; +import org.enso.interpreter.instrument.UpdatesSynchronizationState; import org.enso.interpreter.instrument.execution.LocationFilter; import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode; import org.enso.interpreter.node.expression.builtin.text.util.TypeToDisplayTextNodeGen; @@ -92,6 +93,7 @@ private FunctionCallInstrumentationNode.FunctionCall prepareFunctionCall( * @param call the call metadata. * @param cache the precomputed expression values. * @param methodCallsCache the storage tracking the executed method calls. + * @param syncState the synchronization state of runtime updates. * @param nextExecutionItem the next item scheduled for execution. * @param funCallCallback the consumer for function call events. * @param onComputedCallback the consumer of the computed value events. @@ -103,6 +105,7 @@ public void execute( FunctionCallInstrumentationNode.FunctionCall call, RuntimeCache cache, MethodCallsCache methodCallsCache, + UpdatesSynchronizationState syncState, UUID nextExecutionItem, Consumer funCallCallback, Consumer onComputedCallback, @@ -122,6 +125,7 @@ public void execute( locationFilter, cache, methodCallsCache, + syncState, nextExecutionItem, funCallCallback, onComputedCallback, @@ -143,6 +147,7 @@ public void execute( * @param methodName the method name. * @param cache the precomputed expression values. * @param methodCallsCache the storage tracking the executed method calls. + * @param syncState the synchronization state of runtime updates. * @param nextExecutionItem the next item scheduled for execution. * @param funCallCallback the consumer for function call events. * @param onComputedCallback the consumer of the computed value events. @@ -155,6 +160,7 @@ public void execute( String methodName, RuntimeCache cache, MethodCallsCache methodCallsCache, + UpdatesSynchronizationState syncState, UUID nextExecutionItem, Consumer funCallCallback, Consumer onComputedCallback, @@ -171,6 +177,7 @@ public void execute( call, cache, methodCallsCache, + syncState, nextExecutionItem, funCallCallback, onComputedCallback, diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/CacheInvalidation.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/CacheInvalidation.scala index 861cdef97381..e5e0cd060c0c 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/CacheInvalidation.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/CacheInvalidation.scala @@ -145,17 +145,19 @@ object CacheInvalidation { command: Command, indexes: Set[IndexSelector] ): Unit = { - frames.foreach(frame => run(frame.cache, command, indexes)) + frames.foreach(frame => run(frame.cache, frame.syncState, command, indexes)) } /** Run cache invalidation of a single instrument frame. * * @param cache the cache to invalidate + * @param syncState the synchronization state of runtime updates * @param command the invalidation instruction * @param indexes the list of indexes to invalidate */ private def run( cache: RuntimeCache, + syncState: UpdatesSynchronizationState, command: Command, indexes: Set[IndexSelector] ): Unit = @@ -166,13 +168,14 @@ object CacheInvalidation { case Command.InvalidateKeys(keys) => keys.foreach { key => cache.remove(key) - indexes.foreach(clearIndex(_, cache)) + indexes.foreach(clearIndexKey(key, _, cache)) } case Command.InvalidateStale(scope) => val staleKeys = cache.getKeys.asScala.diff(scope.toSet) staleKeys.foreach { key => cache.remove(key) - indexes.foreach(clearIndex(_, cache)) + indexes.foreach(clearIndexKey(key, _, cache)) + syncState.invalidate(key) } case Command.SetMetadata(metadata) => cache.setWeights(metadata.asJavaWeights) @@ -196,4 +199,29 @@ object CacheInvalidation { case IndexSelector.Calls => cache.clearCalls() } + + /** Clear the key in the selected index. + * + * @param key the index key + * @param selector the selected index + * @param cache the cache to invalidate + */ + private def clearIndexKey( + key: UUID, + selector: IndexSelector, + cache: RuntimeCache + ): Unit = + selector match { + case IndexSelector.All => + cache.removeType(key) + cache.removeWeight(key) + cache.removeCall(key) + case IndexSelector.Weights => + cache.removeWeight(key) + case IndexSelector.Types => + cache.removeType(key) + case IndexSelector.Calls => + cache.removeCall(key) + } + } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextState.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextState.scala index e634c8b4d3d7..0cfa1b1092f8 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextState.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ExecutionContextState.scala @@ -27,11 +27,16 @@ object ExecutionContextState { * * @param item the stack item. * @param cache the cache of this stack frame. + * @param syncState the synchronization state of runtime updates. */ -case class InstrumentFrame(item: StackItem, cache: RuntimeCache) +case class InstrumentFrame( + item: StackItem, + cache: RuntimeCache, + syncState: UpdatesSynchronizationState +) case object InstrumentFrame { def apply(item: StackItem): InstrumentFrame = - new InstrumentFrame(item, new RuntimeCache) + new InstrumentFrame(item, new RuntimeCache, new UpdatesSynchronizationState) } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/EditFileCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/EditFileCmd.scala index 2a205fc8683f..1c020c9e7aa1 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/EditFileCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/EditFileCmd.scala @@ -1,7 +1,5 @@ package org.enso.interpreter.instrument.command -import java.util.logging.Level - import org.enso.interpreter.instrument.execution.RuntimeContext import org.enso.interpreter.instrument.job.{EnsureCompiledJob, ExecuteJob} import org.enso.polyglot.runtime.Runtime.Api @@ -25,8 +23,6 @@ class EditFileCmd(request: Api.EditFileNotification) extends Command(None) { ctx.locking.acquireFileLock(request.path) ctx.locking.acquirePendingEditsLock() try { - ctx.executionService.getLogger - .log(Level.FINE, s"EditFileCmd ${request.path}") ctx.state.pendingEdits.enqueue(request.path, request.edits) ctx.jobControlPlane.abortAllJobs() ctx.jobProcessor.run(new EnsureCompiledJob(Seq(request.path))) @@ -45,7 +41,7 @@ class EditFileCmd(request: Api.EditFileNotification) extends Command(None) { .filter(kv => kv._2.nonEmpty) .mapValues(_.toList) .map { case (contextId, stack) => - new ExecuteJob(contextId, stack, Seq(), sendMethodCallUpdates = false) + new ExecuteJob(contextId, stack) } } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/PopContextCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/PopContextCmd.scala index bfdfaba926a0..ef87b001181b 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/PopContextCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/PopContextCmd.scala @@ -1,5 +1,6 @@ package org.enso.interpreter.instrument.command +import org.enso.interpreter.instrument.InstrumentFrame import org.enso.interpreter.instrument.execution.{Executable, RuntimeContext} import org.enso.interpreter.instrument.job.{EnsureCompiledJob, ExecuteJob} import org.enso.polyglot.runtime.Runtime.Api @@ -61,17 +62,10 @@ class PopContextCmd( ): Future[Unit] = { val stack = ctx.contextManager.getStack(request.contextId) if (stack.nonEmpty) { - val executable = - Executable( - request.contextId, - stack, - Seq(), - sendMethodCallUpdates = true - ) + val executable = Executable(request.contextId, stack) for { - _ <- Future { - ctx.jobProcessor.run(EnsureCompiledJob(executable.stack)) - } + _ <- Future(requireMethodPointersSynchronization(stack)) + _ <- Future(ctx.jobProcessor.run(EnsureCompiledJob(executable.stack))) _ <- ctx.jobProcessor.run(new ExecuteJob(executable)) } yield () } else { @@ -79,4 +73,10 @@ class PopContextCmd( } } + private def requireMethodPointersSynchronization( + stack: Iterable[InstrumentFrame] + ): Unit = { + stack.foreach(_.syncState.clearMethodPointersState()) + } + } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/PushContextCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/PushContextCmd.scala index 67fbe685692a..ecb06014de75 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/PushContextCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/PushContextCmd.scala @@ -73,17 +73,10 @@ class PushContextCmd( ec: ExecutionContext ): Future[Unit] = { if (pushed) { - val stack = ctx.contextManager.getStack(request.contextId) - val executable = Executable( - request.contextId, - stack, - Seq(), - sendMethodCallUpdates = false - ) + val stack = ctx.contextManager.getStack(request.contextId) + val executable = Executable(request.contextId, stack) for { - _ <- Future { - ctx.jobProcessor.run(EnsureCompiledJob(executable.stack)) - } + _ <- Future(ctx.jobProcessor.run(EnsureCompiledJob(executable.stack))) _ <- ctx.jobProcessor.run(new ExecuteJob(executable)) } yield () } else { diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RecomputeContextCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RecomputeContextCmd.scala index 1e753c4d430f..7402c997477b 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RecomputeContextCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RecomputeContextCmd.scala @@ -67,17 +67,10 @@ class RecomputeContextCmd( ec: ExecutionContext ): Future[Unit] = { if (isStackNonEmpty) { - val stack = ctx.contextManager.getStack(request.contextId) - val executable = Executable( - request.contextId, - stack, - Seq(), - sendMethodCallUpdates = false - ) + val stack = ctx.contextManager.getStack(request.contextId) + val executable = Executable(request.contextId, stack) for { - _ <- Future { - ctx.jobProcessor.run(EnsureCompiledJob(executable.stack)) - } + _ <- Future(ctx.jobProcessor.run(EnsureCompiledJob(executable.stack))) _ <- ctx.jobProcessor.run(new ExecuteJob(executable)) } yield () } else { diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RenameProjectCmd.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RenameProjectCmd.scala index abb37eac468a..65342aa20e55 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RenameProjectCmd.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/RenameProjectCmd.scala @@ -54,13 +54,13 @@ class RenameProjectCmd( stack: mutable.Stack[InstrumentFrame] ): Unit = { stack.mapInPlace { - case InstrumentFrame(call: Api.StackItem.ExplicitCall, cache) => + case InstrumentFrame(call: Api.StackItem.ExplicitCall, cache, sync) => val moduleName = QualifiedName .fromString(call.methodPointer.module) .renameProject(projectName) .toString val methodPointer = call.methodPointer.copy(module = moduleName) - InstrumentFrame(call.copy(methodPointer = methodPointer), cache) + InstrumentFrame(call.copy(methodPointer = methodPointer), cache, sync) case item => item } } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/execution/Executable.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/execution/Executable.scala index e339cec0f79b..f53461e2a3b2 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/execution/Executable.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/execution/Executable.scala @@ -9,13 +9,8 @@ import scala.collection.mutable * * @param contextId an identifier of a context to execute * @param stack a call stack that must be executed - * @param updatedVisualisations a list of updated visualisations - * @param sendMethodCallUpdates a flag to send all the method calls of the - * executed frame as a value updates */ case class Executable( contextId: Api.ContextId, - stack: mutable.Stack[InstrumentFrame], - updatedVisualisations: Seq[Api.ExpressionId], - sendMethodCallUpdates: Boolean + stack: mutable.Stack[InstrumentFrame] ) diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala index 520107280bb4..d062fa026437 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala @@ -418,7 +418,7 @@ class EnsureCompiledJob(protected val files: Iterable[File]) stack: Iterable[InstrumentFrame] )(implicit ctx: RuntimeContext): Option[CachePreferenceAnalysis.Metadata] = stack.lastOption flatMap { - case InstrumentFrame(Api.StackItem.ExplicitCall(ptr, _, _), _) => + case InstrumentFrame(Api.StackItem.ExplicitCall(ptr, _, _), _, _) => ctx.executionService.getContext.findModule(ptr.module).toScala.map { module => module.getIr diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ExecuteJob.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ExecuteJob.scala index d743e66cadd9..de9cfb5f5caa 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ExecuteJob.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ExecuteJob.scala @@ -10,15 +10,10 @@ import org.enso.polyglot.runtime.Runtime.Api * * @param contextId an identifier of a context to execute * @param stack a call stack to execute - * @param updatedVisualisations a list of updated visualisations - * @param sendMethodCallUpdates a flag to send all the method calls of the - * executed frame as a value updates */ class ExecuteJob( contextId: UUID, - stack: List[InstrumentFrame], - updatedVisualisations: Seq[UUID], - sendMethodCallUpdates: Boolean + stack: List[InstrumentFrame] ) extends Job[Unit]( List(contextId), isCancellable = true, @@ -28,12 +23,7 @@ class ExecuteJob( ) { def this(exe: Executable) = - this( - exe.contextId, - exe.stack.toList, - exe.updatedVisualisations, - exe.sendMethodCallUpdates - ) + this(exe.contextId, exe.stack.toList) /** @inheritdoc */ override def run(implicit ctx: RuntimeContext): Unit = { @@ -41,13 +31,7 @@ class ExecuteJob( ctx.locking.acquireReadCompilationLock() ctx.executionService.getContext.getThreadManager.enter() try { - val outcome = - ProgramExecutionSupport.runProgram( - contextId, - stack, - updatedVisualisations, - sendMethodCallUpdates - ) + val outcome = ProgramExecutionSupport.runProgram(contextId, stack) outcome.foreach { case diagnostic: Api.ExecutionResult.Diagnostic => ctx.endpoint.sendToClient( diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala index 047e5452a548..37283aa1c193 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala @@ -3,7 +3,7 @@ package org.enso.interpreter.instrument.job import java.io.File import java.util.function.Consumer import java.util.logging.Level -import java.util.{Objects, UUID} +import java.util.UUID import cats.implicits._ import com.oracle.truffle.api.exception.AbstractTruffleException @@ -12,6 +12,7 @@ import org.enso.interpreter.instrument.IdExecutionInstrument.{ ExpressionValue } import org.enso.interpreter.instrument.execution.{ + Completion, ErrorResolver, LocationResolver, RuntimeContext @@ -21,12 +22,14 @@ import org.enso.interpreter.instrument.{ InstrumentFrame, MethodCallsCache, RuntimeCache, + UpdatesSynchronizationState, Visualisation } import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode.FunctionCall import org.enso.interpreter.runtime.data.text.Text import org.enso.interpreter.runtime.error.{DataflowError, PanicSentinel} import org.enso.interpreter.runtime.`type`.Types +import org.enso.interpreter.runtime.control.ThreadInterruptedException import org.enso.interpreter.service.error.{ ConstructorNotFoundException, MethodNotFoundException, @@ -47,34 +50,56 @@ object ProgramExecutionSupport { /** Runs an Enso program. * + * @param contextId an identifier of an execution context * @param executionFrame an execution frame * @param callStack a call stack - * @param onCachedMethodCallCallback a listener of cached method calls - * @param onComputedCallback a listener of computed values - * @param onCachedCallback a listener of cached values - * @param onExceptionalCallback the consumer of the exceptional events. */ @scala.annotation.tailrec final private def executeProgram( + contextId: Api.ContextId, executionFrame: ExecutionFrame, - callStack: List[LocalCallFrame], - onCachedMethodCallCallback: Consumer[ExpressionValue], - onComputedCallback: Consumer[ExpressionValue], - onCachedCallback: Consumer[ExpressionValue], - onExceptionalCallback: Consumer[Exception] + callStack: List[LocalCallFrame] )(implicit ctx: RuntimeContext): Unit = { + val logger = ctx.executionService.getLogger val methodCallsCache = new MethodCallsCache var enterables = Map[UUID, FunctionCall]() - val computedCallback: Consumer[ExpressionValue] = - if (callStack.isEmpty) onComputedCallback else _ => () + + val onCachedMethodCallCallback: Consumer[ExpressionValue] = { value => + logger.log(Level.FINEST, s"ON_CACHED_CALL ${value.getExpressionId}") + sendExpressionUpdate(contextId, executionFrame.syncState, value) + } + + val onCachedValueCallback: Consumer[ExpressionValue] = { value => + if (callStack.isEmpty) { + logger.log(Level.FINEST, s"ON_CACHED_VALUE ${value.getExpressionId}") + sendExpressionUpdate(contextId, executionFrame.syncState, value) + sendVisualisationUpdates(contextId, executionFrame.syncState, value) + } + } + + val onComputedValueCallback: Consumer[ExpressionValue] = { value => + if (callStack.isEmpty) { + logger.log(Level.FINEST, s"ON_COMPUTED ${value.getExpressionId}") + sendExpressionUpdate(contextId, executionFrame.syncState, value) + sendVisualisationUpdates(contextId, executionFrame.syncState, value) + } + } + + val onExceptionalCallback: Consumer[Exception] = { value => + logger.log(Level.FINEST, s"ON_ERROR $value") + sendErrorUpdate(contextId, value) + } + val callablesCallback: Consumer[ExpressionCall] = fun => if (callStack.headOption.exists(_.expressionId == fun.getExpressionId)) { enterables += fun.getExpressionId -> fun.getCall } + executionFrame match { case ExecutionFrame( ExecutionItem.Method(module, cons, function), - cache + cache, + syncState ) => ctx.executionService.execute( module.toString, @@ -82,15 +107,17 @@ object ProgramExecutionSupport { function, cache, methodCallsCache, + syncState, callStack.headOption.map(_.expressionId).orNull, callablesCallback, - computedCallback, - onCachedCallback, + onComputedValueCallback, + onCachedValueCallback, onExceptionalCallback ) case ExecutionFrame( ExecutionItem.CallData(expressionId, callData), - cache + cache, + syncState ) => val module = ctx.executionService.getContext @@ -103,10 +130,11 @@ object ProgramExecutionSupport { callData, cache, methodCallsCache, + syncState, callStack.headOption.map(_.expressionId).orNull, callablesCallback, - computedCallback, - onCachedCallback, + onComputedValueCallback, + onCachedValueCallback, onExceptionalCallback ) } @@ -134,17 +162,13 @@ object ProgramExecutionSupport { case item :: tail => enterables.get(item.expressionId) match { case Some(call) => - executeProgram( + val executionFrame = ExecutionFrame( ExecutionItem.CallData(item.expressionId, call), - item.cache - ), - tail, - onCachedMethodCallCallback, - onComputedCallback, - onCachedCallback, - onExceptionalCallback - ) + item.cache, + item.syncState + ) + executeProgram(contextId, executionFrame, tail) case None => () } @@ -155,24 +179,15 @@ object ProgramExecutionSupport { * * @param contextId an identifier of an execution context * @param stack a call stack - * @param updatedVisualisations a list of updated visualisations - * @param sendMethodCallUpdates a flag to send all the method calls of the - * executed frame as a value updates * @param ctx a runtime context - * @return a diagnostic message + * @return an execution result */ final def runProgram( contextId: Api.ContextId, - stack: List[InstrumentFrame], - updatedVisualisations: Seq[Api.ExpressionId], - sendMethodCallUpdates: Boolean + stack: List[InstrumentFrame] )(implicit ctx: RuntimeContext): Option[Api.ExecutionResult] = { val logger = ctx.executionService.getLogger - logger.log( - Level.FINEST, - s"Run program updatedVisualisations=$updatedVisualisations " + - s"sendMethodCallUpdates=$sendMethodCallUpdates" - ) + logger.log(Level.FINEST, s"Run program $contextId") @scala.annotation.tailrec def unwind( stack: List[InstrumentFrame], @@ -182,36 +197,22 @@ object ProgramExecutionSupport { stack match { case Nil => (explicitCalls.lastOption, localCalls) - case List(InstrumentFrame(call: Api.StackItem.ExplicitCall, cache)) => - (Some(ExecutionFrame(ExecutionItem.Method(call), cache)), localCalls) - case InstrumentFrame(Api.StackItem.LocalCall(id), cache) :: xs => - unwind(xs, explicitCalls, LocalCallFrame(id, cache) :: localCalls) + case List( + InstrumentFrame(call: Api.StackItem.ExplicitCall, cache, sync) + ) => + ( + Some(ExecutionFrame(ExecutionItem.Method(call), cache, sync)), + localCalls + ) + case InstrumentFrame(Api.StackItem.LocalCall(id), cache, sync) :: xs => + unwind( + xs, + explicitCalls, + LocalCallFrame(id, cache, sync) :: localCalls + ) case _ => throw new MatchError(stack) } - val onCachedMethodCallCallback: Consumer[ExpressionValue] = { value => - logger.log(Level.FINEST, s"ON_CACHED_CALL ${value.getExpressionId}") - sendValueUpdate(contextId, value, sendMethodCallUpdates) - } - - val onCachedValueCallback: Consumer[ExpressionValue] = { value => - if (updatedVisualisations.contains(value.getExpressionId)) { - logger.log(Level.FINEST, s"ON_CACHED_VALUE ${value.getExpressionId}") - fireVisualisationUpdates(contextId, value) - } - } - - val onComputedValueCallback: Consumer[ExpressionValue] = { value => - logger.log(Level.FINEST, s"ON_COMPUTED ${value.getExpressionId}") - sendValueUpdate(contextId, value, sendMethodCallUpdates) - fireVisualisationUpdates(contextId, value) - } - - val onExceptionalCallback: Consumer[Exception] = { value => - logger.log(Level.FINEST, s"ON_ERROR $value") - sendErrorUpdate(contextId, value) - } - val (explicitCallOpt, localCalls) = unwind(stack, Nil, Nil) val executionResult = for { stackItem <- Either.fromOption( @@ -220,16 +221,7 @@ object ProgramExecutionSupport { ) _ <- Either - .catchNonFatal( - executeProgram( - stackItem, - localCalls, - onCachedMethodCallCallback, - onComputedValueCallback, - onCachedValueCallback, - onExceptionalCallback - ) - ) + .catchNonFatal(executeProgram(contextId, stackItem, localCalls)) .leftMap(onExecutionError(stackItem.item, _)) } yield () logger.log(Level.FINEST, s"Execution finished: $executionResult") @@ -251,15 +243,14 @@ object ProgramExecutionSupport { case ExecutionItem.CallData(_, call) => call.getFunction.getName } val executionUpdate = getExecutionOutcome(error) - ctx.executionService.getLogger - .log( - Level.WARNING, - s"Error executing a function $itemName. ${error.getMessage}" - ) - executionUpdate.getOrElse( - Api.ExecutionResult - .Failure(s"Error in function $itemName. ${error.getMessage}", None) - ) + val message = error match { + case _: ThreadInterruptedException => + s"Execution of function $itemName interrupted." + case _ => + s"Execution of function $itemName failed. ${error.getMessage}" + } + ctx.executionService.getLogger.log(Level.WARNING, message) + executionUpdate.getOrElse(Api.ExecutionResult.Failure(message, None)) } /** Convert the runtime exception to the corresponding API error messages. @@ -334,16 +325,18 @@ object ProgramExecutionSupport { ) } - private def sendValueUpdate( + private def sendExpressionUpdate( contextId: ContextId, - value: ExpressionValue, - sendMethodCallUpdates: Boolean + syncState: UpdatesSynchronizationState, + value: ExpressionValue )(implicit ctx: RuntimeContext): Unit = { + val expressionId = value.getExpressionId val methodPointer = toMethodPointer(value) if ( - (sendMethodCallUpdates && methodPointer.isDefined) || - !Objects.equals(value.getCallInfo, value.getCachedCallInfo) || - !Objects.equals(value.getType, value.getCachedType) || + !syncState.isExpressionSync(expressionId) || + ( + methodPointer.isDefined && !syncState.isMethodPointerSync(expressionId) + ) || Types.isPanic(value.getType) ) { val payload = value.getValue match { @@ -379,6 +372,11 @@ object ProgramExecutionSupport { ) ) ) + + syncState.setExpressionSync(expressionId) + if (methodPointer.isDefined) { + syncState.setMethodPointerSync(expressionId) + } } } @@ -389,22 +387,26 @@ object ProgramExecutionSupport { * @param value the computed value * @param ctx the runtime context */ - private def fireVisualisationUpdates( + private def sendVisualisationUpdates( contextId: ContextId, + syncState: UpdatesSynchronizationState, value: ExpressionValue )(implicit ctx: RuntimeContext): Unit = { - val visualisations = - ctx.contextManager.findVisualisationForExpression( - contextId, - value.getExpressionId - ) - visualisations foreach { visualisation => - emitVisualisationUpdate( - contextId, - visualisation, - value.getExpressionId, - value.getValue - ) + if (!syncState.isVisualisationSync(value.getExpressionId)) { + val visualisations = + ctx.contextManager.findVisualisationForExpression( + contextId, + value.getExpressionId + ) + visualisations.foreach { visualisation => + sendVisualisationUpdate( + contextId, + syncState, + visualisation, + value.getExpressionId, + value.getValue + ) + } } } @@ -416,13 +418,14 @@ object ProgramExecutionSupport { * @param expressionValue the value of expression to visualise * @param ctx the runtime context */ - def emitVisualisationUpdate( + def sendVisualisationUpdate( contextId: ContextId, + syncState: UpdatesSynchronizationState, visualisation: Visualisation, expressionId: UUID, expressionValue: AnyRef )(implicit ctx: RuntimeContext): Unit = { - val errorMsgOrVisualisationData = + val errorOrVisualisationData = Either .catchNonFatal { ctx.executionService.getLogger.log( @@ -448,10 +451,21 @@ object ProgramExecutionSupport { ) ) } - errorMsgOrVisualisationData match { + val result = errorOrVisualisationData match { + case Left(_: ThreadInterruptedException) => + ctx.executionService.getLogger.log( + Level.WARNING, + s"Visualisation thread interrupted ${visualisation.expressionId}." + ) + Completion.Interrupted + case Left(error) => val message = Option(error.getMessage).getOrElse(error.getClass.getSimpleName) + ctx.executionService.getLogger.log( + Level.WARNING, + s"Visualisation evaluation failed: $message." + ) ctx.endpoint.sendToClient( Api.Response( Api.VisualisationEvaluationFailed( @@ -463,11 +477,12 @@ object ProgramExecutionSupport { ) ) ) + Completion.Done case Right(data) => ctx.executionService.getLogger.log( Level.FINEST, - s"Sending visualisation ${visualisation.expressionId}" + s"Visualisation computed ${visualisation.expressionId}." ) ctx.endpoint.sendToClient( Api.Response( @@ -481,6 +496,10 @@ object ProgramExecutionSupport { ) ) ) + Completion.Done + } + if (result != Completion.Interrupted) { + syncState.setVisualisationSync(expressionId) } } @@ -520,19 +539,23 @@ object ProgramExecutionSupport { * * @param item the executionitem * @param cache the cache of this stack frame + * @param syncState the synchronization state of message updates */ sealed private case class ExecutionFrame( item: ExecutionItem, - cache: RuntimeCache + cache: RuntimeCache, + syncState: UpdatesSynchronizationState ) /** A local call frame defined by the expression id. * * @param expressionId the id of the expression * @param cache the cache of this frame + * @param syncState the synchronization state of message updates */ sealed private case class LocalCallFrame( expressionId: UUID, - cache: RuntimeCache + cache: RuntimeCache, + syncState: UpdatesSynchronizationState ) } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/UpsertVisualisationJob.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/UpsertVisualisationJob.scala index b838a4d63f23..6c274b10c498 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/UpsertVisualisationJob.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/UpsertVisualisationJob.scala @@ -1,7 +1,7 @@ package org.enso.interpreter.instrument.job import cats.implicits._ -import org.enso.interpreter.instrument.Visualisation +import org.enso.interpreter.instrument.{InstrumentFrame, Visualisation} import org.enso.interpreter.instrument.execution.{Executable, RuntimeContext} import org.enso.interpreter.instrument.job.UpsertVisualisationJob.{ EvalFailure, @@ -60,29 +60,31 @@ class UpsertVisualisationJob( .flatMap(frame => Option(frame.cache.get(expressionId))) cachedValue match { case Some(value) => - ProgramExecutionSupport.emitVisualisationUpdate( + ProgramExecutionSupport.sendVisualisationUpdate( config.executionContextId, + stack.headOption.get.syncState, visualisation, expressionId, value ) None case None => - Some( - Executable( - config.executionContextId, - stack, - Seq(expressionId), - sendMethodCallUpdates = false - ) - ) + val stack = ctx.contextManager.getStack(config.executionContextId) + requireVisualisationSynchronization(stack, expressionId) + Some(Executable(config.executionContextId, stack)) } } } finally { ctx.locking.releaseWriteCompilationLock() ctx.locking.releaseContextLock(config.executionContextId) } + } + private def requireVisualisationSynchronization( + stack: Iterable[InstrumentFrame], + expressionId: ExpressionId + ): Unit = { + stack.foreach(_.syncState.setVisualisationUnsync(expressionId)) } private def updateVisualisation( diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala index 461c84b918d5..49ee4edc9f28 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala @@ -10,7 +10,6 @@ import org.enso.polyglot.data.TypeGraph import org.enso.polyglot.runtime.Runtime.Api import org.enso.text.editing.model import org.enso.text.editing.model.TextEdit -import org.enso.text.{ContentVersion, Sha3_224VersionCalculator} import org.graalvm.polyglot.Context import org.graalvm.polyglot.io.MessageEndpoint import org.scalatest.BeforeAndAfterEach @@ -322,9 +321,6 @@ class RuntimeServerTest } - def contentsVersion(content: String): ContentVersion = - Sha3_224VersionCalculator.evalVersion(content) - override protected def beforeEach(): Unit = { context = new TestContext("Test") val Some(Api.Response(_, Api.InitializedNotification())) = context.receive @@ -3007,1095 +3003,6 @@ class RuntimeServerTest context.consumeOut shouldEqual List() } - it should "emit visualisation update when expression is computed" in { - val idMain = context.Main.metadata.addItem(78, 1) - val contents = context.Main.code - val mainFile = context.writeMain(context.Main.code) - val moduleName = "Test.Main" - val visualisationFile = - context.writeInSrcDir("Visualisation", context.Visualisation.code) - - context.send( - Api.Request( - Api.OpenFileNotification( - visualisationFile, - context.Visualisation.code, - true - ) - ) - ) - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // Open the new file - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer(moduleName, "Test.Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - context.receive(6) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - TestMessages.update(contextId, idMain, Constants.INTEGER), - context.executionComplete(contextId) - ) - - // attach visualisation - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - idMain, - Api.VisualisationConfiguration( - contextId, - "Test.Visualisation", - "x -> here.encode x" - ) - ) - ) - ) - val attachVisualisationResponses = context.receive(3) - attachVisualisationResponses should contain allOf ( - Api.Response(requestId, Api.VisualisationAttached()), - context.executionComplete(contextId) - ) - val Some(data) = attachVisualisationResponses.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `idMain` - ), - data - ) - ) => - data - } - data.sameElements("50".getBytes) shouldBe true - - // recompute - context.send( - Api.Request(requestId, Api.RecomputeContextRequest(contextId, None)) - ) - - val recomputeResponses = context.receive(3) - recomputeResponses should contain allOf ( - Api.Response(requestId, Api.RecomputeContextResponse(contextId)), - context.executionComplete(contextId) - ) - val Some(data2) = recomputeResponses.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `idMain` - ), - data - ) - ) => - data - } - data2.sameElements("50".getBytes) shouldBe true - } - - it should "emit visualisation update when expression is cached" in { - val contents = context.Main.code - val mainFile = context.writeMain(context.Main.code) - val moduleName = "Test.Main" - val visualisationFile = - context.writeInSrcDir("Visualisation", context.Visualisation.code) - - context.send( - Api.Request( - Api.OpenFileNotification( - visualisationFile, - context.Visualisation.code, - true - ) - ) - ) - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // Open the new file - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer(moduleName, "Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - context.receive(5) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - context.executionComplete(contextId) - ) - - // attach visualisation - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - context.Main.idMainX, - Api.VisualisationConfiguration( - contextId, - "Test.Visualisation", - "x -> here.encode x" - ) - ) - ) - ) - val attachVisualisationResponses = context.receive(2) - attachVisualisationResponses should contain( - Api.Response(requestId, Api.VisualisationAttached()) - ) - val expectedExpressionId = context.Main.idMainX - val Some(data) = attachVisualisationResponses.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `expectedExpressionId` - ), - data - ) - ) => - data - } - data.sameElements("6".getBytes) shouldBe true - - // recompute - context.send( - Api.Request(requestId, Api.RecomputeContextRequest(contextId, None)) - ) - context.receive(2) should contain allOf ( - Api.Response(requestId, Api.RecomputeContextResponse(contextId)), - context.executionComplete(contextId) - ) - - // recompute invalidating x - context.send( - Api.Request( - requestId, - Api.RecomputeContextRequest( - contextId, - Some( - Api.InvalidatedExpressions.Expressions(Vector(context.Main.idMainX)) - ) - ) - ) - ) - val recomputeResponses2 = context.receive(3) - recomputeResponses2 should contain allOf ( - Api.Response(requestId, Api.RecomputeContextResponse(contextId)), - context.executionComplete(contextId) - ) - val Some(data2) = recomputeResponses2.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `expectedExpressionId` - ), - data - ) - ) => - data - } - data2.sameElements("6".getBytes) shouldBe true - } - - it should "emit visualisation update without value update" in { - val contents = context.Main.code - val moduleName = "Test.Main" - val mainFile = context.writeMain(contents) - val visualisationFile = - context.writeInSrcDir("Visualisation", context.Visualisation.code) - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // open files - context.send( - Api.Request( - Api.OpenFileNotification( - visualisationFile, - context.Visualisation.code, - true - ) - ) - ) - context.receiveNone shouldEqual None - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer(moduleName, "Test.Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - - context.receive(5) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - context.executionComplete(contextId) - ) - - // attach visualization - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - context.Main.idMainX, - Api.VisualisationConfiguration( - contextId, - "Test.Visualisation", - "x -> here.encode x" - ) - ) - ) - ) - val attachVisualisationResponses = context.receive(2) - attachVisualisationResponses should contain( - Api.Response(requestId, Api.VisualisationAttached()) - ) - val expectedExpressionId = context.Main.idMainX - val Some(data) = attachVisualisationResponses.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `expectedExpressionId` - ), - data - ) - ) => - data - } - data.sameElements("6".getBytes) shouldBe true - - // Modify the file - context.send( - Api.Request( - Api.EditFileNotification( - mainFile, - Seq( - TextEdit( - model.Range(model.Position(4, 8), model.Position(4, 9)), - "5" - ) - ) - ) - ) - ) - - val editFileResponse = context.receive(1) - val Some(data1) = editFileResponse.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `expectedExpressionId` - ), - data - ) - ) => - data - } - data1.sameElements("5".getBytes) shouldBe true - } - - it should "be able to modify visualisations" in { - val contents = context.Main.code - val mainFile = context.writeMain(contents) - val visualisationFile = - context.writeInSrcDir("Visualisation", context.Visualisation.code) - - // open files - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - context.send( - Api.Request( - Api.OpenFileNotification( - visualisationFile, - context.Visualisation.code, - true - ) - ) - ) - context.receiveNone shouldEqual None - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer("Test.Main", "Test.Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - context.receive(5) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - context.executionComplete(contextId) - ) - - // attach visualisation - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - context.Main.idMainX, - Api.VisualisationConfiguration( - contextId, - "Test.Visualisation", - "x -> here.encode x" - ) - ) - ) - ) - - val attachVisualisationResponses = context.receive(2) - attachVisualisationResponses should contain( - Api.Response(requestId, Api.VisualisationAttached()) - ) - val expectedExpressionId = context.Main.idMainX - val Some(data) = attachVisualisationResponses.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `expectedExpressionId` - ), - data - ) - ) => - data - } - data.sameElements("6".getBytes) shouldBe true - - // modify visualisation - context.send( - Api.Request( - requestId, - Api.ModifyVisualisation( - visualisationId, - Api.VisualisationConfiguration( - contextId, - "Test.Visualisation", - "x -> here.incAndEncode x" - ) - ) - ) - ) - val modifyVisualisationResponses = context.receive(2) - modifyVisualisationResponses should contain( - Api.Response(requestId, Api.VisualisationModified()) - ) - val Some(dataAfterModification) = - modifyVisualisationResponses.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `expectedExpressionId` - ), - data - ) - ) => - data - } - dataAfterModification.sameElements("7".getBytes) shouldBe true - } - - it should "not emit visualisation updates when visualisation is detached" in { - val contents = context.Main.code - val mainFile = context.writeMain(contents) - val visualisationFile = - context.writeInSrcDir("Visualisation", context.Visualisation.code) - - // open files - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - context.send( - Api.Request( - Api.OpenFileNotification( - visualisationFile, - context.Visualisation.code, - true - ) - ) - ) - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // attach visualisation - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - context.Main.idMainX, - Api.VisualisationConfiguration( - contextId, - "Test.Visualisation", - "x -> here.encode x" - ) - ) - ) - ) - context.receive(3) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.VisualisationAttached()), - Api.Response( - Api.ExecutionFailed( - contextId, - Api.ExecutionResult.Failure("Execution stack is empty.", None) - ) - ), - context.executionComplete(contextId) - ) - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer("Test.Main", "Test.Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - val pushResponses = context.receive(6) - pushResponses should contain allOf ( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - context.executionComplete(contextId) - ) - val expectedExpressionId = context.Main.idMainX - val Some(data) = - pushResponses.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `expectedExpressionId` - ), - data - ) - ) => - data - } - data.sameElements("6".getBytes) shouldBe true - - // detach visualisation - context.send( - Api.Request( - requestId, - Api.DetachVisualisation( - contextId, - visualisationId, - context.Main.idMainX - ) - ) - ) - context.receive shouldEqual Some( - Api.Response(requestId, Api.VisualisationDetached()) - ) - - // recompute - context.send( - Api.Request(requestId, Api.RecomputeContextRequest(contextId, None)) - ) - context.receive(2) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.RecomputeContextResponse(contextId)), - context.executionComplete(contextId) - ) - - // recompute invalidating x - context.send( - Api.Request( - requestId, - Api.RecomputeContextRequest( - contextId, - Some( - Api.InvalidatedExpressions.Expressions(Vector(context.Main.idMainX)) - ) - ) - ) - ) - context.receive(2) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.RecomputeContextResponse(contextId)), - context.executionComplete(contextId) - ) - } - - it should "not reorder visualization commands" in { - val contents = context.Main.code - val mainFile = context.writeMain(contents) - val visualisationFile = - context.writeInSrcDir("Visualisation", context.Visualisation.code) - - // open files - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - context.send( - Api.Request( - Api.OpenFileNotification( - visualisationFile, - context.Visualisation.code, - true - ) - ) - ) - context.receiveNone shouldEqual None - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer("Test.Main", "Test.Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - context.receive(5) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - context.executionComplete(contextId) - ) - - // attach visualisation - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - context.Main.idMainX, - Api.VisualisationConfiguration( - contextId, - "Test.Visualisation", - "x -> here.encode x" - ) - ) - ) - ) - - val attachVisualisationResponses = context.receive(2) - attachVisualisationResponses should contain( - Api.Response(requestId, Api.VisualisationAttached()) - ) - val expectedExpressionId = context.Main.idMainX - val Some(data) = attachVisualisationResponses.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `expectedExpressionId` - ), - data - ) - ) => - data - } - data.sameElements("6".getBytes) shouldBe true - - // modify visualisation - context.send( - Api.Request( - requestId, - Api.ModifyVisualisation( - visualisationId, - Api.VisualisationConfiguration( - contextId, - "Test.Visualisation", - "x -> here.incAndEncode x" - ) - ) - ) - ) - // detach visualisation - context.send( - Api.Request( - requestId, - Api.DetachVisualisation( - contextId, - visualisationId, - context.Main.idMainX - ) - ) - ) - val modifyVisualisationResponses = context.receive(3) - modifyVisualisationResponses should contain allOf ( - Api.Response(requestId, Api.VisualisationModified()), - Api.Response(requestId, Api.VisualisationDetached()) - ) - val Some(dataAfterModification) = - modifyVisualisationResponses.collectFirst { - case Api.Response( - None, - Api.VisualisationUpdate( - Api.VisualisationContext( - `visualisationId`, - `contextId`, - `expectedExpressionId` - ), - data - ) - ) => - data - } - dataAfterModification.sameElements("7".getBytes) shouldBe true - } - - it should "return ModuleNotFound error when attaching visualisation" in { - val idMain = context.Main.metadata.addItem(78, 1) - val contents = context.Main.code - val mainFile = context.writeMain(context.Main.code) - val moduleName = "Test.Main" - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // Open the new file - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer(moduleName, "Test.Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - context.receive(6) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - TestMessages.update(contextId, idMain, Constants.INTEGER), - context.executionComplete(contextId) - ) - - // attach visualisation - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - idMain, - Api.VisualisationConfiguration( - contextId, - "Test.Undefined", - "x -> x" - ) - ) - ) - ) - context.receive(1) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.ModuleNotFound("Test.Undefined")) - ) - } - - it should "return VisualisationExpressionFailed error when attaching visualisation" in { - val idMain = context.Main.metadata.addItem(78, 1) - val contents = context.Main.code - val mainFile = context.writeMain(context.Main.code) - val moduleName = "Test.Main" - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // Open the new file - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer(moduleName, "Test.Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - context.receive(6) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - TestMessages.update(contextId, idMain, Constants.INTEGER), - context.executionComplete(contextId) - ) - - // attach visualisation - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - idMain, - Api.VisualisationConfiguration( - contextId, - "Test.Main", - "here.does_not_exist" - ) - ) - ) - ) - context.receive(1) should contain theSameElementsAs Seq( - Api.Response( - requestId, - Api.VisualisationExpressionFailed( - "Method `does_not_exist` of Main could not be found.", - Some( - Api.ExecutionResult.Diagnostic.error( - message = "Method `does_not_exist` of Main could not be found.", - stack = Vector( - Api.StackTraceElement("", None, None, None), - Api.StackTraceElement("Debug.eval", None, None, None) - ) - ) - ) - ) - ) - ) - } - - it should "return visualisation evaluation errors with diagnostic info" in { - val idMain = context.Main.metadata.addItem(78, 1) - val contents = context.Main.code - val mainFile = context.writeMain(context.Main.code) - val moduleName = "Test.Main" - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // Open the new file - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer(moduleName, "Test.Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - context.receive(6) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - TestMessages.update(contextId, idMain, Constants.INTEGER), - context.executionComplete(contextId) - ) - - // attach visualisation - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - idMain, - Api.VisualisationConfiguration( - contextId, - moduleName, - "x -> x.visualise_me" - ) - ) - ) - ) - context.receive(3) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.VisualisationAttached()), - Api.Response( - Api.VisualisationEvaluationFailed( - contextId, - visualisationId, - idMain, - "Method `visualise_me` of 50 (Integer) could not be found.", - Some( - Api.ExecutionResult.Diagnostic.error( - "Method `visualise_me` of 50 (Integer) could not be found.", - None, - Some(model.Range(model.Position(0, 5), model.Position(0, 19))), - None, - Vector( - Api.StackTraceElement( - "", - None, - Some( - model.Range(model.Position(0, 5), model.Position(0, 19)) - ), - None - ) - ) - ) - ) - ) - ), - context.executionComplete(contextId) - ) - } - - it should "return visualisation error with a stack trace" in { - val idMain = context.Main.metadata.addItem(78, 1) - val contents = context.Main.code - val mainFile = context.writeMain(context.Main.code) - val moduleName = "Test.Main" - val visualisationCode = - """ - |encode x = x.visualise_me - | - |inc_and_encode x = here.encode x+1 - |""".stripMargin.linesIterator.mkString("\n") - - val visualisationFile = - context.writeInSrcDir("Visualisation", visualisationCode) - - context.send( - Api.Request( - Api.OpenFileNotification( - visualisationFile, - visualisationCode, - true - ) - ) - ) - - val contextId = UUID.randomUUID() - val requestId = UUID.randomUUID() - val visualisationId = UUID.randomUUID() - - // create context - context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) - context.receive shouldEqual Some( - Api.Response(requestId, Api.CreateContextResponse(contextId)) - ) - - // Open the new file - context.send( - Api.Request(Api.OpenFileNotification(mainFile, contents, true)) - ) - context.receiveNone shouldEqual None - - // push main - val item1 = Api.StackItem.ExplicitCall( - Api.MethodPointer(moduleName, "Test.Main", "main"), - None, - Vector() - ) - context.send( - Api.Request(requestId, Api.PushContextRequest(contextId, item1)) - ) - context.receive(6) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.PushContextResponse(contextId)), - context.Main.Update.mainX(contextId), - context.Main.Update.mainY(contextId), - context.Main.Update.mainZ(contextId), - TestMessages.update(contextId, idMain, Constants.INTEGER), - context.executionComplete(contextId) - ) - - // attach visualisation - context.send( - Api.Request( - requestId, - Api.AttachVisualisation( - visualisationId, - idMain, - Api.VisualisationConfiguration( - contextId, - "Test.Visualisation", - "here.inc_and_encode" - ) - ) - ) - ) - context.receive(3) should contain theSameElementsAs Seq( - Api.Response(requestId, Api.VisualisationAttached()), - Api.Response( - Api.VisualisationEvaluationFailed( - contextId, - visualisationId, - idMain, - "Method `visualise_me` of 51 (Integer) could not be found.", - Some( - Api.ExecutionResult.Diagnostic.error( - "Method `visualise_me` of 51 (Integer) could not be found.", - Some(visualisationFile), - Some(model.Range(model.Position(1, 11), model.Position(1, 25))), - None, - Vector( - Api.StackTraceElement( - "Visualisation.encode", - Some(visualisationFile), - Some( - model.Range(model.Position(1, 11), model.Position(1, 25)) - ), - None - ), - Api.StackTraceElement( - "Visualisation.inc_and_encode", - Some(visualisationFile), - Some( - model.Range(model.Position(3, 19), model.Position(3, 34)) - ), - None - ) - ) - ) - ) - ) - ), - context.executionComplete(contextId) - ) - } - it should "rename a project" in { val contents = context.Main.code val mainFile = context.writeMain(contents) diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeVisualisationsTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeVisualisationsTest.scala new file mode 100644 index 000000000000..110a8e092d79 --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeVisualisationsTest.scala @@ -0,0 +1,1583 @@ +package org.enso.interpreter.test.instrument + +import java.io.{ByteArrayOutputStream, File} +import java.nio.ByteBuffer +import java.nio.file.Files +import java.util.UUID +import java.util.concurrent.{LinkedBlockingQueue, TimeUnit} + +import org.enso.interpreter.instrument.execution.Timer +import org.enso.interpreter.runtime.`type`.Constants +import org.enso.interpreter.runtime.{Context => EnsoContext} +import org.enso.interpreter.test.Metadata +import org.enso.pkg.{Package, PackageManager} +import org.enso.polyglot._ +import org.enso.polyglot.runtime.Runtime.Api +import org.enso.text.editing.model +import org.enso.text.editing.model.TextEdit +import org.graalvm.polyglot.Context +import org.graalvm.polyglot.io.MessageEndpoint +import org.scalatest.BeforeAndAfterEach +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +@scala.annotation.nowarn("msg=multiarg infix syntax") +class RuntimeVisualisationsTest + extends AnyFlatSpec + with Matchers + with BeforeAndAfterEach { + + // === Test Timer =========================================================== + + class TestTimer extends Timer { + override def getTime(): Long = 0 + } + + // === Test Utilities ======================================================= + + var context: TestContext = _ + + class TestContext(packageName: String) { + var endPoint: MessageEndpoint = _ + val messageQueue: LinkedBlockingQueue[Api.Response] = + new LinkedBlockingQueue() + + val tmpDir: File = Files.createTempDirectory("enso-test-packages").toFile + + val pkg: Package[File] = + PackageManager.Default.create(tmpDir, packageName, "0.0.1") + val out: ByteArrayOutputStream = new ByteArrayOutputStream() + val executionContext = new PolyglotContext( + Context + .newBuilder(LanguageInfo.ID) + .allowExperimentalOptions(true) + .allowAllAccess(true) + .option(RuntimeOptions.PACKAGES_PATH, pkg.root.getAbsolutePath) + .option(RuntimeOptions.LOG_LEVEL, "WARNING") + .option(RuntimeOptions.INTERPRETER_SEQUENTIAL_COMMAND_EXECUTION, "true") + .option(RuntimeOptions.ENABLE_PROJECT_SUGGESTIONS, "false") + .option(RuntimeOptions.ENABLE_GLOBAL_SUGGESTIONS, "false") + .option(RuntimeServerInfo.ENABLE_OPTION, "true") + .out(out) + .serverTransport { (uri, peer) => + if (uri.toString == RuntimeServerInfo.URI) { + endPoint = peer + new MessageEndpoint { + override def sendText(text: String): Unit = {} + + override def sendBinary(data: ByteBuffer): Unit = + Api.deserializeResponse(data).foreach(messageQueue.add) + + override def sendPing(data: ByteBuffer): Unit = {} + + override def sendPong(data: ByteBuffer): Unit = {} + + override def sendClose(): Unit = {} + } + } else null + } + .build() + ) + executionContext.context.initialize(LanguageInfo.ID) + + val languageContext = executionContext.context + .getBindings(LanguageInfo.ID) + .invokeMember(MethodNames.TopScope.LEAK_CONTEXT) + .asHostObject[EnsoContext] + languageContext.getLanguage.getIdExecutionInstrument + .overrideTimer(new TestTimer) + + def writeMain(contents: String): File = + Files.write(pkg.mainFile.toPath, contents.getBytes).toFile + + def writeFile(file: File, contents: String): File = + Files.write(file.toPath, contents.getBytes).toFile + + def writeInSrcDir(moduleName: String, contents: String): File = { + val file = new File(pkg.sourceDir, s"$moduleName.enso") + Files.write(file.toPath, contents.getBytes).toFile + } + + def send(msg: Api.Request): Unit = endPoint.sendBinary(Api.serialize(msg)) + + def receiveNone: Option[Api.Response] = { + Option(messageQueue.poll()) + } + + def receive: Option[Api.Response] = { + Option(messageQueue.poll(10, TimeUnit.SECONDS)) + } + + def receive(n: Int): List[Api.Response] = { + Iterator.continually(receive).take(n).flatten.toList + } + + def consumeOut: List[String] = { + val result = out.toString + out.reset() + result.linesIterator.toList + } + + def executionComplete(contextId: UUID): Api.Response = + Api.Response(Api.ExecutionComplete(contextId)) + + // === The Tests ========================================================== + + object Main { + + val metadata = new Metadata + + val idMainX = metadata.addItem(42, 1) + val idMainY = metadata.addItem(52, 7) + val idMainZ = metadata.addItem(68, 5) + val idFooY = metadata.addItem(107, 8) + val idFooZ = metadata.addItem(124, 5) + + def code = + metadata.appendToCode( + """ + |from Builtins import all + | + |main = + | x = 6 + | y = x.foo 5 + | z = y + 5 + | z + | + |Number.foo = x -> + | y = this + 3 + | z = y * x + | z + |""".stripMargin.linesIterator.mkString("\n") + ) + + object Update { + + def mainX(contextId: UUID, fromCache: Boolean = false): Api.Response = + Api.Response( + Api.ExpressionUpdates( + contextId, + Set( + Api.ExpressionUpdate( + Main.idMainX, + Some(Constants.INTEGER), + None, + Vector(Api.ProfilingInfo.ExecutionTime(0)), + fromCache, + Api.ExpressionUpdate.Payload.Value() + ) + ) + ) + ) + + def mainY(contextId: UUID, fromCache: Boolean = false): Api.Response = + Api.Response( + Api.ExpressionUpdates( + contextId, + Set( + Api.ExpressionUpdate( + Main.idMainY, + Some(Constants.INTEGER), + Some(Api.MethodPointer("Test.Main", Constants.NUMBER, "foo")), + Vector(Api.ProfilingInfo.ExecutionTime(0)), + fromCache, + Api.ExpressionUpdate.Payload.Value() + ) + ) + ) + ) + + def mainZ(contextId: UUID, fromCache: Boolean = false): Api.Response = + Api.Response( + Api.ExpressionUpdates( + contextId, + Set( + Api.ExpressionUpdate( + Main.idMainZ, + Some(Constants.INTEGER), + None, + Vector(Api.ProfilingInfo.ExecutionTime(0)), + fromCache, + Api.ExpressionUpdate.Payload.Value() + ) + ) + ) + ) + + def fooY(contextId: UUID, fromCache: Boolean = false): Api.Response = + Api.Response( + Api.ExpressionUpdates( + contextId, + Set( + Api.ExpressionUpdate( + Main.idFooY, + Some(Constants.INTEGER), + None, + Vector(Api.ProfilingInfo.ExecutionTime(0)), + fromCache, + Api.ExpressionUpdate.Payload.Value() + ) + ) + ) + ) + + def fooZ(contextId: UUID, fromCache: Boolean = false): Api.Response = + Api.Response( + Api.ExpressionUpdates( + contextId, + Set( + Api.ExpressionUpdate( + Main.idFooZ, + Some(Constants.INTEGER), + None, + Vector(Api.ProfilingInfo.ExecutionTime(0)), + fromCache, + Api.ExpressionUpdate.Payload.Value() + ) + ) + ) + ) + } + } + + object Visualisation { + + val code = + """ + |encode = x -> x.to_text + | + |incAndEncode = x -> here.encode x+1 + | + |""".stripMargin + + } + + } + + override protected def beforeEach(): Unit = { + context = new TestContext("Test") + val Some(Api.Response(_, Api.InitializedNotification())) = context.receive + } + + it should "emit visualisation update when expression is computed" in { + val idMain = context.Main.metadata.addItem(78, 1) + val contents = context.Main.code + val mainFile = context.writeMain(context.Main.code) + val moduleName = "Test.Main" + val visualisationFile = + context.writeInSrcDir("Visualisation", context.Visualisation.code) + + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.Visualisation.code, + true + ) + ) + ) + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // Open the new file + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receive(6) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + TestMessages.update(contextId, idMain, Constants.INTEGER), + context.executionComplete(contextId) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + idMain, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "x -> here.encode x" + ) + ) + ) + ) + val attachVisualisationResponses = context.receive(3) + attachVisualisationResponses should contain allOf ( + Api.Response(requestId, Api.VisualisationAttached()), + context.executionComplete(contextId) + ) + val Some(data) = attachVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `idMain` + ), + data + ) + ) => + data + } + data.sameElements("50".getBytes) shouldBe true + + // recompute + context.send( + Api.Request(requestId, Api.RecomputeContextRequest(contextId, None)) + ) + + val recomputeResponses = context.receive(3) + recomputeResponses should contain allOf ( + Api.Response(requestId, Api.RecomputeContextResponse(contextId)), + context.executionComplete(contextId) + ) + val Some(data2) = recomputeResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `idMain` + ), + data + ) + ) => + data + } + data2.sameElements("50".getBytes) shouldBe true + } + + it should "emit visualisation update when expression is cached" in { + val contents = context.Main.code + val mainFile = context.writeMain(context.Main.code) + val moduleName = "Test.Main" + val visualisationFile = + context.writeInSrcDir("Visualisation", context.Visualisation.code) + + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.Visualisation.code, + true + ) + ) + ) + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // Open the new file + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receive(5) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + context.executionComplete(contextId) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + context.Main.idMainX, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "x -> here.encode x" + ) + ) + ) + ) + val attachVisualisationResponses = context.receive(2) + attachVisualisationResponses should contain( + Api.Response(requestId, Api.VisualisationAttached()) + ) + val expectedExpressionId = context.Main.idMainX + val Some(data) = attachVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data.sameElements("6".getBytes) shouldBe true + + // recompute + context.send( + Api.Request(requestId, Api.RecomputeContextRequest(contextId, None)) + ) + context.receive(2) should contain allOf ( + Api.Response(requestId, Api.RecomputeContextResponse(contextId)), + context.executionComplete(contextId) + ) + + // recompute invalidating x + context.send( + Api.Request( + requestId, + Api.RecomputeContextRequest( + contextId, + Some( + Api.InvalidatedExpressions.Expressions(Vector(context.Main.idMainX)) + ) + ) + ) + ) + val recomputeResponses2 = context.receive(3) + recomputeResponses2 should contain allOf ( + Api.Response(requestId, Api.RecomputeContextResponse(contextId)), + context.executionComplete(contextId) + ) + val Some(data2) = recomputeResponses2.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data2.sameElements("6".getBytes) shouldBe true + } + + it should "emit visualisation update when expression is modified" in { + val contents = context.Main.code + val moduleName = "Test.Main" + val mainFile = context.writeMain(contents) + val visualisationFile = + context.writeInSrcDir("Visualisation", context.Visualisation.code) + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // open files + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.Visualisation.code, + true + ) + ) + ) + context.receiveNone shouldEqual None + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + + context.receive(5) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + context.executionComplete(contextId) + ) + + // attach visualization + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + context.Main.idMainX, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "x -> here.encode x" + ) + ) + ) + ) + val attachVisualisationResponses = context.receive(2) + attachVisualisationResponses should contain( + Api.Response(requestId, Api.VisualisationAttached()) + ) + val expectedExpressionId = context.Main.idMainX + val Some(data) = attachVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data.sameElements("6".getBytes) shouldBe true + + // Modify the file + context.send( + Api.Request( + Api.EditFileNotification( + mainFile, + Seq( + TextEdit( + model.Range(model.Position(4, 8), model.Position(4, 9)), + "5" + ) + ) + ) + ) + ) + + val editFileResponse = context.receive(2) + editFileResponse should contain( + context.executionComplete(contextId) + ) + val Some(data1) = editFileResponse.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data1.sameElements("5".getBytes) shouldBe true + } + + it should "emit visualisation update when transitive expression is modified" in { + val contents = context.Main.code + val moduleName = "Test.Main" + val mainFile = context.writeMain(contents) + val visualisationFile = + context.writeInSrcDir("Visualisation", context.Visualisation.code) + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // open files + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.Visualisation.code, + true + ) + ) + ) + context.receiveNone shouldEqual None + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + + context.receive(5) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + context.executionComplete(contextId) + ) + + // attach visualization + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + context.Main.idMainZ, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "here.encode" + ) + ) + ) + ) + val attachVisualisationResponses = context.receive(2) + attachVisualisationResponses should contain( + Api.Response(requestId, Api.VisualisationAttached()) + ) + val expectedExpressionId = context.Main.idMainZ + val Some(data) = attachVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data.sameElements("50".getBytes) shouldBe true + + // Modify the file + context.send( + Api.Request( + Api.EditFileNotification( + mainFile, + Seq( + TextEdit( + model.Range(model.Position(4, 8), model.Position(4, 9)), + "5" + ) + ) + ) + ) + ) + + val editFileResponse = context.receive(2) + editFileResponse should contain( + context.executionComplete(contextId) + ) + val Some(data1) = editFileResponse.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data1.sameElements("45".getBytes) shouldBe true + } + + it should "be able to modify visualisations" in { + val contents = context.Main.code + val mainFile = context.writeMain(contents) + val visualisationFile = + context.writeInSrcDir("Visualisation", context.Visualisation.code) + + // open files + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.Visualisation.code, + true + ) + ) + ) + context.receiveNone shouldEqual None + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer("Test.Main", "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receive(5) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + context.executionComplete(contextId) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + context.Main.idMainX, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "x -> here.encode x" + ) + ) + ) + ) + + val attachVisualisationResponses = context.receive(2) + attachVisualisationResponses should contain( + Api.Response(requestId, Api.VisualisationAttached()) + ) + val expectedExpressionId = context.Main.idMainX + val Some(data) = attachVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data.sameElements("6".getBytes) shouldBe true + + // modify visualisation + context.send( + Api.Request( + requestId, + Api.ModifyVisualisation( + visualisationId, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "x -> here.incAndEncode x" + ) + ) + ) + ) + val modifyVisualisationResponses = context.receive(2) + modifyVisualisationResponses should contain( + Api.Response(requestId, Api.VisualisationModified()) + ) + val Some(dataAfterModification) = + modifyVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + dataAfterModification.sameElements("7".getBytes) shouldBe true + } + + it should "not emit visualisation update when visualisation is detached" in { + val contents = context.Main.code + val mainFile = context.writeMain(contents) + val visualisationFile = + context.writeInSrcDir("Visualisation", context.Visualisation.code) + + // open files + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.Visualisation.code, + true + ) + ) + ) + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + context.Main.idMainX, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "x -> here.encode x" + ) + ) + ) + ) + context.receive(3) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.VisualisationAttached()), + Api.Response( + Api.ExecutionFailed( + contextId, + Api.ExecutionResult.Failure("Execution stack is empty.", None) + ) + ), + context.executionComplete(contextId) + ) + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer("Test.Main", "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + val pushResponses = context.receive(6) + pushResponses should contain allOf ( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + context.executionComplete(contextId) + ) + val expectedExpressionId = context.Main.idMainX + val Some(data) = + pushResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data.sameElements("6".getBytes) shouldBe true + + // detach visualisation + context.send( + Api.Request( + requestId, + Api.DetachVisualisation( + contextId, + visualisationId, + context.Main.idMainX + ) + ) + ) + context.receive shouldEqual Some( + Api.Response(requestId, Api.VisualisationDetached()) + ) + + // recompute + context.send( + Api.Request(requestId, Api.RecomputeContextRequest(contextId, None)) + ) + context.receive(2) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.RecomputeContextResponse(contextId)), + context.executionComplete(contextId) + ) + + // recompute invalidating x + context.send( + Api.Request( + requestId, + Api.RecomputeContextRequest( + contextId, + Some( + Api.InvalidatedExpressions.Expressions(Vector(context.Main.idMainX)) + ) + ) + ) + ) + context.receive(2) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.RecomputeContextResponse(contextId)), + context.executionComplete(contextId) + ) + } + + it should "not emit visualisation update when expression is not affected by the change" in { + val contents = context.Main.code + val moduleName = "Test.Main" + val mainFile = context.writeMain(contents) + val visualisationFile = + context.writeInSrcDir("Visualisation", context.Visualisation.code) + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // open files + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.Visualisation.code, + true + ) + ) + ) + context.receiveNone shouldEqual None + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + + context.receive(5) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + context.executionComplete(contextId) + ) + + // attach visualization + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + context.Main.idMainX, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "here.encode" + ) + ) + ) + ) + val attachVisualisationResponses = context.receive(2) + attachVisualisationResponses should contain( + Api.Response(requestId, Api.VisualisationAttached()) + ) + val expectedExpressionId = context.Main.idMainX + val Some(data) = attachVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data.sameElements("6".getBytes) shouldBe true + + // Modify the file + context.send( + Api.Request( + Api.EditFileNotification( + mainFile, + Seq( + TextEdit( + model.Range(model.Position(6, 12), model.Position(6, 13)), + "6" + ) + ) + ) + ) + ) + + context.receive(1) should contain theSameElementsAs Seq( + context.executionComplete(contextId) + ) + } + + it should "not reorder visualization commands" in { + val contents = context.Main.code + val mainFile = context.writeMain(contents) + val visualisationFile = + context.writeInSrcDir("Visualisation", context.Visualisation.code) + + // open files + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + context.Visualisation.code, + true + ) + ) + ) + context.receiveNone shouldEqual None + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer("Test.Main", "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receive(5) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + context.executionComplete(contextId) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + context.Main.idMainX, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "x -> here.encode x" + ) + ) + ) + ) + + val attachVisualisationResponses = context.receive(2) + attachVisualisationResponses should contain( + Api.Response(requestId, Api.VisualisationAttached()) + ) + val expectedExpressionId = context.Main.idMainX + val Some(data) = attachVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + data.sameElements("6".getBytes) shouldBe true + + // modify visualisation + context.send( + Api.Request( + requestId, + Api.ModifyVisualisation( + visualisationId, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "x -> here.incAndEncode x" + ) + ) + ) + ) + // detach visualisation + context.send( + Api.Request( + requestId, + Api.DetachVisualisation( + contextId, + visualisationId, + context.Main.idMainX + ) + ) + ) + val modifyVisualisationResponses = context.receive(3) + modifyVisualisationResponses should contain allOf ( + Api.Response(requestId, Api.VisualisationModified()), + Api.Response(requestId, Api.VisualisationDetached()) + ) + val Some(dataAfterModification) = + modifyVisualisationResponses.collectFirst { + case Api.Response( + None, + Api.VisualisationUpdate( + Api.VisualisationContext( + `visualisationId`, + `contextId`, + `expectedExpressionId` + ), + data + ) + ) => + data + } + dataAfterModification.sameElements("7".getBytes) shouldBe true + } + + it should "return ModuleNotFound error when attaching visualisation" in { + val idMain = context.Main.metadata.addItem(78, 1) + val contents = context.Main.code + val mainFile = context.writeMain(context.Main.code) + val moduleName = "Test.Main" + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // Open the new file + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receive(6) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + TestMessages.update(contextId, idMain, Constants.INTEGER), + context.executionComplete(contextId) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + idMain, + Api.VisualisationConfiguration( + contextId, + "Test.Undefined", + "x -> x" + ) + ) + ) + ) + context.receive(1) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.ModuleNotFound("Test.Undefined")) + ) + } + + it should "return VisualisationExpressionFailed error when attaching visualisation" in { + val idMain = context.Main.metadata.addItem(78, 1) + val contents = context.Main.code + val mainFile = context.writeMain(context.Main.code) + val moduleName = "Test.Main" + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // Open the new file + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receive(6) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + TestMessages.update(contextId, idMain, Constants.INTEGER), + context.executionComplete(contextId) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + idMain, + Api.VisualisationConfiguration( + contextId, + "Test.Main", + "here.does_not_exist" + ) + ) + ) + ) + context.receive(1) should contain theSameElementsAs Seq( + Api.Response( + requestId, + Api.VisualisationExpressionFailed( + "Method `does_not_exist` of Main could not be found.", + Some( + Api.ExecutionResult.Diagnostic.error( + message = "Method `does_not_exist` of Main could not be found.", + stack = Vector( + Api.StackTraceElement("", None, None, None), + Api.StackTraceElement("Debug.eval", None, None, None) + ) + ) + ) + ) + ) + ) + } + + it should "return visualisation evaluation errors with diagnostic info" in { + val idMain = context.Main.metadata.addItem(78, 1) + val contents = context.Main.code + val mainFile = context.writeMain(context.Main.code) + val moduleName = "Test.Main" + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // Open the new file + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receive(6) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + TestMessages.update(contextId, idMain, Constants.INTEGER), + context.executionComplete(contextId) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + idMain, + Api.VisualisationConfiguration( + contextId, + moduleName, + "x -> x.visualise_me" + ) + ) + ) + ) + context.receive(3) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.VisualisationAttached()), + Api.Response( + Api.VisualisationEvaluationFailed( + contextId, + visualisationId, + idMain, + "Method `visualise_me` of 50 (Integer) could not be found.", + Some( + Api.ExecutionResult.Diagnostic.error( + "Method `visualise_me` of 50 (Integer) could not be found.", + None, + Some(model.Range(model.Position(0, 5), model.Position(0, 19))), + None, + Vector( + Api.StackTraceElement( + "", + None, + Some( + model.Range(model.Position(0, 5), model.Position(0, 19)) + ), + None + ) + ) + ) + ) + ) + ), + context.executionComplete(contextId) + ) + } + + it should "return visualisation error with a stack trace" in { + val idMain = context.Main.metadata.addItem(78, 1) + val contents = context.Main.code + val mainFile = context.writeMain(context.Main.code) + val moduleName = "Test.Main" + val visualisationCode = + """ + |encode x = x.visualise_me + | + |inc_and_encode x = here.encode x+1 + |""".stripMargin.linesIterator.mkString("\n") + + val visualisationFile = + context.writeInSrcDir("Visualisation", visualisationCode) + + context.send( + Api.Request( + Api.OpenFileNotification( + visualisationFile, + visualisationCode, + true + ) + ) + ) + + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val visualisationId = UUID.randomUUID() + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // Open the new file + context.send( + Api.Request(Api.OpenFileNotification(mainFile, contents, true)) + ) + context.receiveNone shouldEqual None + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, "Test.Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + context.receive(6) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + context.Main.Update.mainX(contextId), + context.Main.Update.mainY(contextId), + context.Main.Update.mainZ(contextId), + TestMessages.update(contextId, idMain, Constants.INTEGER), + context.executionComplete(contextId) + ) + + // attach visualisation + context.send( + Api.Request( + requestId, + Api.AttachVisualisation( + visualisationId, + idMain, + Api.VisualisationConfiguration( + contextId, + "Test.Visualisation", + "here.inc_and_encode" + ) + ) + ) + ) + context.receive(3) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.VisualisationAttached()), + Api.Response( + Api.VisualisationEvaluationFailed( + contextId, + visualisationId, + idMain, + "Method `visualise_me` of 51 (Integer) could not be found.", + Some( + Api.ExecutionResult.Diagnostic.error( + "Method `visualise_me` of 51 (Integer) could not be found.", + Some(visualisationFile), + Some(model.Range(model.Position(1, 11), model.Position(1, 25))), + None, + Vector( + Api.StackTraceElement( + "Visualisation.encode", + Some(visualisationFile), + Some( + model.Range(model.Position(1, 11), model.Position(1, 25)) + ), + None + ), + Api.StackTraceElement( + "Visualisation.inc_and_encode", + Some(visualisationFile), + Some( + model.Range(model.Position(3, 19), model.Position(3, 34)) + ), + None + ) + ) + ) + ) + ) + ), + context.executionComplete(contextId) + ) + } +}