mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 22:01:42 +03:00
Integrate the LS with context management (#657)
This commit is contained in:
parent
86fdc07ce0
commit
75f25b66db
@ -1272,7 +1272,7 @@ be correlated between the textual and data connections.
|
||||
```
|
||||
|
||||
##### Errors
|
||||
- [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror) to signal
|
||||
- [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror) to signal
|
||||
that session is already initialised.
|
||||
|
||||
#### `session/initDataConnection`
|
||||
@ -2404,10 +2404,12 @@ null
|
||||
```
|
||||
|
||||
##### Errors
|
||||
- [`StackItemNotFoundError`](#stackitemnotfounderror) when the request stack
|
||||
item could not be found.
|
||||
- [`AccessDeniedError`](#accessdeniederror) when the user does not hold the
|
||||
`executionContext/canModify` capability for this context.
|
||||
- [`StackItemNotFoundError`](#stackitemnotfounderror) when the request stack
|
||||
item could not be found.
|
||||
- [`InvalidStackItemError`](#invalidstackitemerror) when pushing `LocalCall` on
|
||||
top of the empty stack, or pushing `ExplicitCall` on top of non-empty stack.
|
||||
|
||||
|
||||
#### `executionContext/pop`
|
||||
@ -2708,6 +2710,16 @@ It signals that stack is empty.
|
||||
}
|
||||
```
|
||||
|
||||
##### `InvalidStackItemError`
|
||||
It signals that stack is invalid in this context.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 2004,
|
||||
"message" : "Invalid stack item"
|
||||
}
|
||||
```
|
||||
|
||||
##### `FileNotOpenedError`
|
||||
Signals that a file wasn't opened.
|
||||
|
||||
|
@ -25,7 +25,7 @@ import org.enso.languageserver.protocol.{JsonRpc, ServerClientControllerFactory}
|
||||
import org.enso.languageserver.runtime.{ContextRegistry, RuntimeConnector}
|
||||
import org.enso.languageserver.text.BufferRegistry
|
||||
import org.enso.languageserver.LanguageServer
|
||||
import org.enso.polyglot.{LanguageInfo, RuntimeServerInfo}
|
||||
import org.enso.polyglot.{LanguageInfo, RuntimeOptions, RuntimeServerInfo}
|
||||
import org.graalvm.polyglot.Context
|
||||
import org.graalvm.polyglot.io.MessageEndpoint
|
||||
|
||||
@ -103,6 +103,7 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
.allowAllAccess(true)
|
||||
.allowExperimentalOptions(true)
|
||||
.option(RuntimeServerInfo.ENABLE_OPTION, "true")
|
||||
.option(RuntimeOptions.getPackagesPathOption, serverConfig.contentRootPath)
|
||||
.serverTransport((uri: URI, peerEndpoint: MessageEndpoint) => {
|
||||
if (uri.toString == RuntimeServerInfo.URI) {
|
||||
val connection = new RuntimeConnector.Endpoint(
|
||||
|
@ -22,20 +22,22 @@ class PingHandler(
|
||||
|
||||
import context.dispatcher
|
||||
|
||||
private var cancellable: Option[Cancellable] = None
|
||||
|
||||
override def receive: Receive = scatter
|
||||
|
||||
private def scatter: Receive = {
|
||||
case Request(MonitoringApi.Ping, id, Unused) =>
|
||||
subsystems.foreach(_ ! Ping)
|
||||
val cancellable =
|
||||
cancellable = Some(
|
||||
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
|
||||
context.become(gather(id, sender(), cancellable))
|
||||
)
|
||||
context.become(gather(id, sender()))
|
||||
}
|
||||
|
||||
private def gather(
|
||||
id: Id,
|
||||
replyTo: ActorRef,
|
||||
cancellable: Cancellable,
|
||||
count: Int = 0
|
||||
): Receive = {
|
||||
case RequestTimeout =>
|
||||
@ -47,13 +49,16 @@ class PingHandler(
|
||||
case Pong =>
|
||||
if (count + 1 == subsystems.size) {
|
||||
replyTo ! ResponseResult(MonitoringApi.Ping, id, Unused)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
} else {
|
||||
context.become(gather(id, replyTo, cancellable, count + 1))
|
||||
context.become(gather(id, replyTo, count + 1))
|
||||
}
|
||||
}
|
||||
|
||||
override def postStop(): Unit = {
|
||||
cancellable.foreach(_.cancel())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object PingHandler {
|
||||
|
@ -43,6 +43,8 @@ final class ContextEventsListener(
|
||||
contextId,
|
||||
updates
|
||||
)
|
||||
case _: Api.ExpressionValuesComputed =>
|
||||
// ignore updates from other contexts
|
||||
}
|
||||
|
||||
private def toRuntimeUpdate(
|
||||
@ -73,7 +75,7 @@ final class ContextEventsListener(
|
||||
private def toRuntimePointer(
|
||||
pointer: Api.MethodPointer
|
||||
): Option[MethodPointer] =
|
||||
config.findRelativePath(pointer.file.toFile).map { relativePath =>
|
||||
config.findRelativePath(pointer.file).map { relativePath =>
|
||||
MethodPointer(
|
||||
file = relativePath,
|
||||
definedOnType = pointer.definedOnType,
|
||||
|
@ -125,7 +125,7 @@ final class ContextRegistry(config: Config, runtime: ActorRef)
|
||||
): Either[FileSystemFailure, Api.MethodPointer] =
|
||||
config.findContentRoot(pointer.file.rootId).map { rootPath =>
|
||||
Api.MethodPointer(
|
||||
file = pointer.file.toFile(rootPath).toPath,
|
||||
file = pointer.file.toFile(rootPath),
|
||||
definedOnType = pointer.definedOnType,
|
||||
name = pointer.name
|
||||
)
|
||||
|
@ -112,4 +112,11 @@ object ContextRegistryProtocol {
|
||||
* @param contextId execution context identifier
|
||||
*/
|
||||
case class EmptyStackError(contextId: ContextId) extends Failure
|
||||
|
||||
/**
|
||||
* Signals that stack item is invalid in this context.
|
||||
*
|
||||
* @param contextId execution context identifier
|
||||
*/
|
||||
case class InvalidStackItemError(contextId: ContextId) extends Failure
|
||||
}
|
||||
|
@ -85,4 +85,6 @@ object ExecutionApi {
|
||||
|
||||
case object EmptyStackError extends Error(2003, "Stack is empty")
|
||||
|
||||
case object InvalidStackItemError extends Error(2004, "Invalid stack item")
|
||||
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ object RuntimeFailureMapper {
|
||||
FileSystemFailureMapper.mapFailure(error)
|
||||
case ContextRegistryProtocol.EmptyStackError(_) =>
|
||||
EmptyStackError
|
||||
case ContextRegistryProtocol.InvalidStackItemError(_) =>
|
||||
InvalidStackItemError
|
||||
}
|
||||
|
||||
/**
|
||||
@ -38,6 +40,8 @@ object RuntimeFailureMapper {
|
||||
ContextRegistryProtocol.ContextNotFound(contextId)
|
||||
case Api.EmptyStackError(contextId) =>
|
||||
ContextRegistryProtocol.EmptyStackError(contextId)
|
||||
case Api.InvalidStackItemError(contextId) =>
|
||||
ContextRegistryProtocol.InvalidStackItemError(contextId)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.enso.languageserver.filemanager
|
||||
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
import java.util.concurrent.{Executors, LinkedBlockingQueue}
|
||||
import java.util.concurrent.{Executors, LinkedBlockingQueue, Semaphore}
|
||||
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.languageserver.effect.Effects
|
||||
@ -65,17 +65,23 @@ class WatcherAdapterSpec extends AnyFlatSpec with Matchers with Effects {
|
||||
def withWatcher(
|
||||
test: (Path, LinkedBlockingQueue[WatcherEvent]) => Any
|
||||
): Any = {
|
||||
val lock = new Semaphore(0)
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val tmp = Files.createTempDirectory(null).toRealPath()
|
||||
val queue = new LinkedBlockingQueue[WatcherEvent]()
|
||||
val watcher = WatcherAdapter.build(tmp, queue.put(_), println(_))
|
||||
|
||||
executor.submit(new Runnable {
|
||||
def run() = watcher.start().unsafeRunSync(): Unit
|
||||
def run(): Unit = {
|
||||
lock.release()
|
||||
watcher.start().unsafeRunSync(): Unit
|
||||
}
|
||||
})
|
||||
|
||||
try test(tmp, queue)
|
||||
finally {
|
||||
try {
|
||||
lock.tryAcquire(Timeout.length, Timeout.unit)
|
||||
test(tmp, queue)
|
||||
} finally {
|
||||
watcher.stop().unsafeRunSync()
|
||||
executor.shutdown()
|
||||
Try(executor.awaitTermination(Timeout.length, Timeout.unit))
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.languageserver.websocket
|
||||
|
||||
import java.nio.file.Paths
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
import io.circe.literal._
|
||||
@ -252,6 +252,54 @@ class ContextRegistryTest extends BaseServerTest {
|
||||
""")
|
||||
}
|
||||
|
||||
"return InvalidStackItemError when pushing invalid item to stack" in {
|
||||
val client = getInitialisedWsClient()
|
||||
// create context
|
||||
client.send(json.executionContextCreateRequest(1))
|
||||
val (requestId1, contextId) =
|
||||
runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(requestId, Api.CreateContextRequest(contextId)) =>
|
||||
(requestId, contextId)
|
||||
case msg =>
|
||||
fail(s"Unexpected message: $msg")
|
||||
}
|
||||
runtimeConnectorProbe.lastSender ! Api.Response(
|
||||
requestId1,
|
||||
Api.CreateContextResponse(contextId)
|
||||
)
|
||||
client.expectJson(json.executionContextCreateResponse(1, contextId))
|
||||
|
||||
// push invalid item
|
||||
val expressionId = UUID.randomUUID()
|
||||
client.send(json.executionContextPushRequest(2, contextId, expressionId))
|
||||
val requestId2 =
|
||||
runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
`contextId`,
|
||||
Api.StackItem.LocalCall(`expressionId`)
|
||||
)
|
||||
) =>
|
||||
requestId
|
||||
case msg =>
|
||||
fail(s"Unexpected message: $msg")
|
||||
}
|
||||
runtimeConnectorProbe.lastSender ! Api.Response(
|
||||
requestId2,
|
||||
Api.InvalidStackItemError(contextId)
|
||||
)
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id" : 2,
|
||||
"error" : {
|
||||
"code" : 2004,
|
||||
"message" : "Invalid stack item"
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"send notifications" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
@ -277,7 +325,7 @@ class ContextRegistryTest extends BaseServerTest {
|
||||
shortValue = Some("ShortValue"),
|
||||
methodCall = Some(
|
||||
Api.MethodPointer(
|
||||
file = testContentRoot,
|
||||
file = testContentRoot.toFile,
|
||||
definedOnType = "DefinedOnType",
|
||||
name = "Name"
|
||||
)
|
||||
@ -288,7 +336,7 @@ class ContextRegistryTest extends BaseServerTest {
|
||||
expressionType = None,
|
||||
shortValue = None,
|
||||
methodCall = Some(
|
||||
Api.MethodPointer(Paths.get("/invalid"), "Invalid", "Invalid")
|
||||
Api.MethodPointer(new File("/invalid"), "Invalid", "Invalid")
|
||||
)
|
||||
)
|
||||
system.eventStream.publish(
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.enso.polyglot.runtime
|
||||
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
|
||||
@ -66,14 +66,13 @@ object Runtime {
|
||||
value = classOf[Api.EmptyStackError],
|
||||
name = "emptyStackError"
|
||||
),
|
||||
new JsonSubTypes.Type(value = classOf[Api.Execute], name = "execute"),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.InvalidStackItemError],
|
||||
name = "invalidStackItemError"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.InitializedNotification],
|
||||
name = "initializedNotification"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.ExpressionValueUpdateNotification],
|
||||
name = "expressionValueUpdateNotification"
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -96,7 +95,7 @@ object Runtime {
|
||||
/**
|
||||
* A representation of a pointer to a method definition.
|
||||
*/
|
||||
case class MethodPointer(file: Path, definedOnType: String, name: String)
|
||||
case class MethodPointer(file: File, definedOnType: String, name: String)
|
||||
|
||||
/**
|
||||
* A representation of an executable position in code.
|
||||
@ -274,6 +273,13 @@ object Runtime {
|
||||
*/
|
||||
case class EmptyStackError(contextId: ContextId) extends Error
|
||||
|
||||
/**
|
||||
* An error response signifying that stack item is invalid.
|
||||
*
|
||||
* @param contextId the context's id
|
||||
*/
|
||||
case class InvalidStackItemError(contextId: ContextId) extends Error
|
||||
|
||||
/**
|
||||
* Notification sent from the server to the client upon successful
|
||||
* initialization. Any messages sent to the server before receiving this
|
||||
@ -281,38 +287,6 @@ object Runtime {
|
||||
*/
|
||||
case class InitializedNotification() extends ApiResponse
|
||||
|
||||
/**
|
||||
* An execution request for a given method.
|
||||
* Note that this is a temporary message, only used to test functionality.
|
||||
* To be replaced with actual execution stack API.
|
||||
*
|
||||
* @param modName the module to look for the method.
|
||||
* @param consName the constructor the method is defined on.
|
||||
* @param funName the method name.
|
||||
* @param enterExprs the expressions that should be "entered" after
|
||||
* executing the base method.
|
||||
*/
|
||||
case class Execute(
|
||||
modName: String,
|
||||
consName: String,
|
||||
funName: String,
|
||||
enterExprs: List[ExpressionId]
|
||||
) extends ApiRequest
|
||||
|
||||
/**
|
||||
* A notification sent from the server whenever an expression value
|
||||
* becomes available.
|
||||
* Note this is a temporary message, only used to test functionality.
|
||||
* To be replaced with actual value computed notifications.
|
||||
*
|
||||
* @param expressionId the id of computed expression.
|
||||
* @param shortValue the string representation of the expression's value.
|
||||
*/
|
||||
case class ExpressionValueUpdateNotification(
|
||||
expressionId: ExpressionId,
|
||||
shortValue: String
|
||||
) extends ApiResponse
|
||||
|
||||
private lazy val mapper = {
|
||||
val factory = new CBORFactory()
|
||||
val mapper = new ObjectMapper(factory) with ScalaObjectMapper
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.enso.interpreter.instrument;
|
||||
|
||||
import com.oracle.truffle.api.CallTarget;
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.Truffle;
|
||||
import com.oracle.truffle.api.frame.FrameInstance;
|
||||
import com.oracle.truffle.api.frame.FrameInstanceVisitor;
|
||||
@ -10,7 +11,9 @@ import com.oracle.truffle.api.nodes.Node;
|
||||
import org.enso.interpreter.node.ExpressionNode;
|
||||
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode;
|
||||
import org.enso.interpreter.runtime.tag.IdentifiedTag;
|
||||
import org.enso.interpreter.runtime.type.Types;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@ -34,7 +37,7 @@ public class IdExecutionInstrument extends TruffleInstrument {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
/** A value class for notifications about functions being called in the course of execution. */
|
||||
/** A class for notifications about functions being called in the course of execution. */
|
||||
public static class ExpressionCall {
|
||||
private UUID expressionId;
|
||||
private FunctionCallInstrumentationNode.FunctionCall call;
|
||||
@ -61,19 +64,22 @@ public class IdExecutionInstrument extends TruffleInstrument {
|
||||
}
|
||||
}
|
||||
|
||||
/** A value class for notifications about identified expressions' values being computed. */
|
||||
/** A class for notifications about identified expressions' values being computed. */
|
||||
public static class ExpressionValue {
|
||||
private UUID expressionId;
|
||||
private Object value;
|
||||
private final UUID expressionId;
|
||||
private final String type;
|
||||
private final Object value;
|
||||
|
||||
/**
|
||||
* Creates a new instance of this class.
|
||||
*
|
||||
* @param expressionId the id of the expression being computed.
|
||||
* @param type of the computed expression.
|
||||
* @param value the value returned by computing the expression.
|
||||
*/
|
||||
public ExpressionValue(UUID expressionId, Object value) {
|
||||
public ExpressionValue(UUID expressionId, String type, Object value) {
|
||||
this.expressionId = expressionId;
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@ -82,6 +88,12 @@ public class IdExecutionInstrument extends TruffleInstrument {
|
||||
return expressionId;
|
||||
}
|
||||
|
||||
/** @return the computed type of the expression. */
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
public Optional<String> getType() {
|
||||
return Optional.ofNullable(type);
|
||||
}
|
||||
|
||||
/** @return the computed value of the expression. */
|
||||
public Object getValue() {
|
||||
return value;
|
||||
@ -166,7 +178,8 @@ public class IdExecutionInstrument extends TruffleInstrument {
|
||||
(FunctionCallInstrumentationNode.FunctionCall) result));
|
||||
} else if (node instanceof ExpressionNode) {
|
||||
valueCallback.accept(
|
||||
new ExpressionValue(((ExpressionNode) context.getInstrumentedNode()).getId(), result));
|
||||
new ExpressionValue(
|
||||
((ExpressionNode) node).getId(), Types.getName(result).orElse(null), result));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import org.enso.interpreter.runtime.scope.ModuleScope;
|
||||
import org.enso.interpreter.runtime.scope.TopLevelScope;
|
||||
import org.enso.interpreter.util.ScalaConversions;
|
||||
import org.enso.pkg.Package;
|
||||
import org.enso.pkg.QualifiedName;
|
||||
|
||||
/**
|
||||
* The language context is the internal state of the language that is associated with each thread in
|
||||
@ -28,6 +29,7 @@ public class Context {
|
||||
private final Env environment;
|
||||
private final Compiler compiler;
|
||||
private final PrintStream out;
|
||||
private List<Package> packages;
|
||||
|
||||
/**
|
||||
* Creates a new Enso context.
|
||||
@ -41,12 +43,17 @@ public class Context {
|
||||
this.out = new PrintStream(environment.out());
|
||||
|
||||
List<File> packagePaths = OptionsHelper.getPackagesPaths(environment);
|
||||
Map<String, Module> knownFiles =
|
||||
|
||||
packages =
|
||||
packagePaths.stream()
|
||||
.map(Package::fromDirectory)
|
||||
.map(ScalaConversions::asJava)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Module> knownFiles =
|
||||
packages.stream()
|
||||
.flatMap(p -> ScalaConversions.asJava(p.listSources()).stream())
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
@ -124,6 +131,33 @@ public class Context {
|
||||
initializeScope(scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess module name from the file path by comparing it with the source pathes
|
||||
* of imported packages.
|
||||
*
|
||||
* @param path file path.
|
||||
* @return qualified module name if the function can find imported package
|
||||
* with matching path.
|
||||
*/
|
||||
public Optional<QualifiedName> getModuleNameForFile(File path) {
|
||||
return packages.stream()
|
||||
.filter(pkg -> path.getAbsolutePath().startsWith(pkg.sourceDir().getAbsolutePath()))
|
||||
.map(pkg -> pkg.moduleNameForFile(path))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get module from the file path. Function tries to recover module name from
|
||||
* the provided file path.
|
||||
*
|
||||
* @param path file path.
|
||||
* @return module if module name can be guessed from the provided file path.
|
||||
*/
|
||||
public Optional<Module> getModuleForFile(File path) {
|
||||
return getModuleNameForFile(path)
|
||||
.flatMap(n -> compiler().topScope().getModule(n.toString()));
|
||||
}
|
||||
|
||||
private void initializeScope(ModuleScope scope) {
|
||||
scope.addImport(getBuiltins().getScope());
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
|
||||
import org.enso.interpreter.runtime.callable.function.Function;
|
||||
import org.enso.interpreter.runtime.error.RuntimeError;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* This class defines the interpreter-level type system for Enso.
|
||||
*
|
||||
@ -23,6 +25,7 @@ import org.enso.interpreter.runtime.error.RuntimeError;
|
||||
*/
|
||||
@TypeSystem({
|
||||
long.class,
|
||||
String.class,
|
||||
Function.class,
|
||||
Atom.class,
|
||||
AtomConstructor.class,
|
||||
@ -89,6 +92,33 @@ public class Types {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a type of the given object as a string.
|
||||
*
|
||||
* @param value an object of interest.
|
||||
* @return the string representation of object's type.
|
||||
*/
|
||||
public static Optional<String> getName(Object value) {
|
||||
if (TypesGen.isLong(value)) {
|
||||
return Optional.of("Number");
|
||||
} else if (TypesGen.isString(value)) {
|
||||
return Optional.of("Text");
|
||||
} else if (TypesGen.isFunction(value)) {
|
||||
return Optional.of("Function");
|
||||
} else if (TypesGen.isAtom(value)) {
|
||||
return Optional.of(TypesGen.asAtom(value).getConstructor().getName());
|
||||
} else if (TypesGen.isAtomConstructor(value)) {
|
||||
return Optional.of(TypesGen.asAtomConstructor(value).getName());
|
||||
} else if (TypesGen.isThunk(value)) {
|
||||
return Optional.of("Thunk");
|
||||
} else if (TypesGen.isRuntimeError(value)) {
|
||||
return Optional
|
||||
.of("Error " + TypesGen.asRuntimeError(value).getPayload().toString());
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the arguments array has exactly one element of a given type and extracts it.
|
||||
*
|
||||
|
@ -14,7 +14,9 @@ import org.enso.interpreter.runtime.Module;
|
||||
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
|
||||
import org.enso.interpreter.runtime.callable.function.Function;
|
||||
import org.enso.interpreter.runtime.scope.ModuleScope;
|
||||
import org.enso.pkg.QualifiedName;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@ -93,24 +95,26 @@ public class ExecutionService {
|
||||
* Executes a method described by its name, constructor it's defined on and the module it's
|
||||
* defined in.
|
||||
*
|
||||
* @param moduleName the qualified name of the module the method is defined in.
|
||||
* @param modulePath the path to the module where the method is defined.
|
||||
* @param consName the name of the constructor the method is defined on.
|
||||
* @param methodName the method name.
|
||||
* @param valueCallback the consumer for expression value events.
|
||||
* @param funCallCallback the consumer for function call events.
|
||||
*/
|
||||
public void execute(
|
||||
String moduleName,
|
||||
File modulePath,
|
||||
String consName,
|
||||
String methodName,
|
||||
Consumer<IdExecutionInstrument.ExpressionValue> valueCallback,
|
||||
Consumer<IdExecutionInstrument.ExpressionCall> funCallCallback)
|
||||
throws UnsupportedMessageException, ArityException, UnsupportedTypeException {
|
||||
Optional<FunctionCallInstrumentationNode.FunctionCall> callMay =
|
||||
prepareFunctionCall(moduleName, consName, methodName);
|
||||
Optional<FunctionCallInstrumentationNode.FunctionCall> callMay = context
|
||||
.getModuleNameForFile(modulePath)
|
||||
.flatMap(moduleName -> prepareFunctionCall(moduleName.toString(), consName, methodName));
|
||||
if (!callMay.isPresent()) {
|
||||
return;
|
||||
}
|
||||
execute(callMay.get(), valueCallback, funCallCallback);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -43,6 +43,15 @@ class ExecutionContextManager {
|
||||
_ <- contexts.get(ExecutionContext(id))
|
||||
} yield ExecutionContext(id)
|
||||
|
||||
/**
|
||||
* Gets a stack for a given context id.
|
||||
*
|
||||
* @param id the context id.
|
||||
* @return the stack.
|
||||
*/
|
||||
def getStack(id: ContextId): Stack[StackItem] =
|
||||
contexts.getOrElse(ExecutionContext(id), Stack())
|
||||
|
||||
/**
|
||||
* If the context exists, push the item on the stack.
|
||||
*
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.enso.interpeter.instrument
|
||||
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
import java.util.function.Consumer
|
||||
@ -14,6 +15,8 @@ import org.enso.interpreter.service.ExecutionService
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.graalvm.polyglot.io.MessageEndpoint
|
||||
|
||||
import scala.jdk.javaapi.OptionConverters
|
||||
|
||||
/**
|
||||
* A message endpoint implementation used by the
|
||||
* [[org.enso.interpreter.instrument.RuntimeServerInstrument]].
|
||||
@ -53,7 +56,7 @@ class Endpoint(handler: Handler) extends MessageEndpoint {
|
||||
* A message handler, dispatching behaviors based on messages received
|
||||
* from an instance of [[Endpoint]].
|
||||
*/
|
||||
class Handler {
|
||||
final class Handler {
|
||||
val endpoint = new Endpoint(this)
|
||||
val contextManager = new ExecutionContextManager
|
||||
|
||||
@ -80,37 +83,50 @@ class Handler {
|
||||
|
||||
private object ExecutionItem {
|
||||
case class Method(
|
||||
module: String,
|
||||
file: File,
|
||||
constructor: String,
|
||||
function: String
|
||||
) extends ExecutionItem
|
||||
|
||||
case class CallData(callData: FunctionCall) extends ExecutionItem
|
||||
}
|
||||
private def sendVal(res: ExpressionValue): Unit = {
|
||||
|
||||
private def sendUpdate(
|
||||
contextId: Api.ContextId,
|
||||
res: ExpressionValue
|
||||
): Unit = {
|
||||
endpoint.sendToClient(
|
||||
Api.Response(
|
||||
Api.ExpressionValueUpdateNotification(
|
||||
res.getExpressionId,
|
||||
res.getValue.toString
|
||||
Api.ExpressionValuesComputed(
|
||||
contextId,
|
||||
Vector(
|
||||
Api.ExpressionValueUpdate(
|
||||
res.getExpressionId,
|
||||
OptionConverters.toScala(res.getType),
|
||||
Some(res.getValue.toString),
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@scala.annotation.tailrec
|
||||
private def execute(
|
||||
executionItem: ExecutionItem,
|
||||
furtherStack: List[UUID]
|
||||
callStack: List[UUID],
|
||||
valueCallback: Consumer[ExpressionValue]
|
||||
): Unit = {
|
||||
var enterables: Map[UUID, FunctionCall] = Map()
|
||||
val valsCallback: Consumer[ExpressionValue] =
|
||||
if (furtherStack.isEmpty) sendVal else _ => ()
|
||||
if (callStack.isEmpty) valueCallback else _ => ()
|
||||
val callablesCallback: Consumer[ExpressionCall] = fun =>
|
||||
enterables += fun.getExpressionId -> fun.getCall
|
||||
executionItem match {
|
||||
case ExecutionItem.Method(module, cons, function) =>
|
||||
case ExecutionItem.Method(file, cons, function) =>
|
||||
executionService.execute(
|
||||
module,
|
||||
file,
|
||||
cons,
|
||||
function,
|
||||
valsCallback,
|
||||
@ -120,15 +136,49 @@ class Handler {
|
||||
executionService.execute(callData, valsCallback, callablesCallback)
|
||||
}
|
||||
|
||||
furtherStack match {
|
||||
callStack match {
|
||||
case Nil => ()
|
||||
case item :: tail =>
|
||||
enterables
|
||||
.get(item)
|
||||
.foreach(call => execute(ExecutionItem.CallData(call), tail))
|
||||
enterables.get(item) match {
|
||||
case Some(call) =>
|
||||
execute(ExecutionItem.CallData(call), tail, valueCallback)
|
||||
case None =>
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def execute(
|
||||
contextId: Api.ContextId,
|
||||
stack: List[Api.StackItem]
|
||||
): Unit = {
|
||||
def unwind(
|
||||
stack: List[Api.StackItem],
|
||||
explicitCalls: List[Api.StackItem.ExplicitCall],
|
||||
localCalls: List[UUID]
|
||||
): (List[Api.StackItem.ExplicitCall], List[UUID]) =
|
||||
stack match {
|
||||
case Nil =>
|
||||
(explicitCalls, localCalls)
|
||||
case List(call: Api.StackItem.ExplicitCall) =>
|
||||
(List(call), localCalls)
|
||||
case Api.StackItem.LocalCall(id) :: xs =>
|
||||
unwind(xs, explicitCalls, id :: localCalls)
|
||||
}
|
||||
val (explicitCalls, localCalls) = unwind(stack, Nil, Nil)
|
||||
val item = toExecutionItem(explicitCalls.head)
|
||||
execute(item, localCalls, sendUpdate(contextId, _))
|
||||
}
|
||||
|
||||
private def toExecutionItem(
|
||||
call: Api.StackItem.ExplicitCall
|
||||
): ExecutionItem =
|
||||
ExecutionItem.Method(
|
||||
call.methodPointer.file,
|
||||
call.methodPointer.definedOnType,
|
||||
call.methodPointer.name
|
||||
)
|
||||
|
||||
private def withContext(action: => Unit): Unit = {
|
||||
val token = truffleContext.enter()
|
||||
try {
|
||||
@ -163,18 +213,19 @@ class Handler {
|
||||
}
|
||||
|
||||
case Api.Request(requestId, Api.PushContextRequest(contextId, item)) => {
|
||||
val payload = contextManager.push(contextId, item) match {
|
||||
case Some(()) => Api.PushContextResponse(contextId)
|
||||
case None => Api.ContextNotExistError(contextId)
|
||||
}
|
||||
endpoint.sendToClient(Api.Response(requestId, payload))
|
||||
}
|
||||
|
||||
case Api.Request(requestId, Api.PopContextRequest(contextId)) =>
|
||||
if (contextManager.get(contextId).isDefined) {
|
||||
val payload = contextManager.pop(contextId) match {
|
||||
case Some(_) => Api.PopContextResponse(contextId)
|
||||
case None => Api.EmptyStackError(contextId)
|
||||
val stack = contextManager.getStack(contextId)
|
||||
val payload = item match {
|
||||
case call: Api.StackItem.ExplicitCall if stack.isEmpty =>
|
||||
contextManager.push(contextId, item)
|
||||
withContext(execute(contextId, List(call)))
|
||||
Api.PushContextResponse(contextId)
|
||||
case _: Api.StackItem.LocalCall if stack.nonEmpty =>
|
||||
contextManager.push(contextId, item)
|
||||
withContext(execute(contextId, stack.toList))
|
||||
Api.PushContextResponse(contextId)
|
||||
case _ =>
|
||||
Api.InvalidStackItemError(contextId)
|
||||
}
|
||||
endpoint.sendToClient(Api.Response(requestId, payload))
|
||||
} else {
|
||||
@ -182,9 +233,26 @@ class Handler {
|
||||
Api.Response(requestId, Api.ContextNotExistError(contextId))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
case Api.Request(_, Api.Execute(mod, cons, fun, furtherStack)) =>
|
||||
withContext(execute(ExecutionItem.Method(mod, cons, fun), furtherStack))
|
||||
|
||||
case Api.Request(requestId, Api.PopContextRequest(contextId)) =>
|
||||
if (contextManager.get(contextId).isDefined) {
|
||||
val payload = contextManager.pop(contextId) match {
|
||||
case Some(_: Api.StackItem.ExplicitCall) =>
|
||||
Api.PopContextResponse(contextId)
|
||||
case Some(_: Api.StackItem.LocalCall) =>
|
||||
withContext(
|
||||
execute(contextId, contextManager.getStack(contextId).toList)
|
||||
)
|
||||
Api.PopContextResponse(contextId)
|
||||
case None =>
|
||||
Api.EmptyStackError(contextId)
|
||||
}
|
||||
endpoint.sendToClient(Api.Response(requestId, payload))
|
||||
} else {
|
||||
endpoint.sendToClient(
|
||||
Api.Response(requestId, Api.ContextNotExistError(contextId))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,138 +0,0 @@
|
||||
package org.enso.interpreter.test.instrument
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.polyglot.{LanguageInfo, RuntimeServerInfo}
|
||||
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
|
||||
|
||||
class ContextManagementTest
|
||||
extends AnyFlatSpec
|
||||
with Matchers
|
||||
with BeforeAndAfterEach {
|
||||
|
||||
var context: Context = _
|
||||
var messageQueue: List[Api.Response] = _
|
||||
var endPoint: MessageEndpoint = _
|
||||
|
||||
override protected def beforeEach(): Unit = {
|
||||
messageQueue = List()
|
||||
context = Context
|
||||
.newBuilder(LanguageInfo.ID)
|
||||
.allowExperimentalOptions(true)
|
||||
.option(RuntimeServerInfo.ENABLE_OPTION, "true")
|
||||
.serverTransport { (uri, peer) =>
|
||||
if (uri.toString == RuntimeServerInfo.URI) {
|
||||
endPoint = peer
|
||||
new MessageEndpoint {
|
||||
override def sendText(text: String): Unit = {}
|
||||
|
||||
override def sendBinary(data: ByteBuffer): Unit =
|
||||
messageQueue ++= Api.deserializeResponse(data)
|
||||
|
||||
override def sendPing(data: ByteBuffer): Unit = {}
|
||||
|
||||
override def sendPong(data: ByteBuffer): Unit = {}
|
||||
|
||||
override def sendClose(): Unit = {}
|
||||
}
|
||||
} else null
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
def send(msg: Api.Request): Unit = endPoint.sendBinary(Api.serialize(msg))
|
||||
def receive: Option[Api.Response] = {
|
||||
val msg = messageQueue.headOption
|
||||
messageQueue = messageQueue.drop(1)
|
||||
msg
|
||||
}
|
||||
|
||||
"Runtime server" should "allow context creation and deletion" in {
|
||||
val requestId1 = UUID.randomUUID()
|
||||
val requestId2 = UUID.randomUUID()
|
||||
val contextId = UUID.randomUUID()
|
||||
send(Api.Request(requestId1, Api.CreateContextRequest(contextId)))
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId1, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
send(Api.Request(requestId2, Api.DestroyContextRequest(contextId)))
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId2, Api.DestroyContextResponse(contextId))
|
||||
)
|
||||
}
|
||||
|
||||
"Runtime server" should "fail destroying a context if it does not exist" in {
|
||||
val requestId1 = UUID.randomUUID()
|
||||
val contextId1 = UUID.randomUUID()
|
||||
val requestId2 = UUID.randomUUID()
|
||||
val contextId2 = UUID.randomUUID()
|
||||
send(Api.Request(requestId1, Api.CreateContextRequest(contextId1)))
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId1, Api.CreateContextResponse(contextId1))
|
||||
)
|
||||
send(Api.Request(requestId2, Api.DestroyContextRequest(contextId2)))
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId2, Api.ContextNotExistError(contextId2))
|
||||
)
|
||||
}
|
||||
|
||||
"Runtime server" should "push and pop the context stack" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val expressionId = UUID.randomUUID()
|
||||
val requestId1 = UUID.randomUUID()
|
||||
val requestId2 = UUID.randomUUID()
|
||||
val requestId3 = UUID.randomUUID()
|
||||
send(Api.Request(requestId1, Api.CreateContextRequest(contextId)))
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId1, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
send(
|
||||
Api.Request(
|
||||
requestId2,
|
||||
Api.PushContextRequest(contextId, Api.StackItem.LocalCall(expressionId))
|
||||
)
|
||||
)
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId2, Api.PushContextResponse(contextId))
|
||||
)
|
||||
send(Api.Request(requestId3, Api.PopContextRequest(contextId)))
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId3, Api.PopContextResponse(contextId))
|
||||
)
|
||||
}
|
||||
|
||||
"Runtime server" should "fail pushing context stack if it doesn't exist" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val expressionId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(contextId, Api.StackItem.LocalCall(expressionId))
|
||||
)
|
||||
)
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.ContextNotExistError(contextId))
|
||||
)
|
||||
}
|
||||
|
||||
"Runtime server" should "fail popping empty stack" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId1 = UUID.randomUUID()
|
||||
val requestId2 = UUID.randomUUID()
|
||||
send(Api.Request(requestId1, Api.CreateContextRequest(contextId)))
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId1, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
send(Api.Request(requestId2, Api.PopContextRequest(contextId)))
|
||||
receive shouldEqual Some(
|
||||
Api.Response(requestId2, Api.EmptyStackError(contextId))
|
||||
)
|
||||
}
|
||||
}
|
@ -60,14 +60,8 @@ class RuntimeServerTest
|
||||
)
|
||||
executionContext.context.initialize(LanguageInfo.ID)
|
||||
|
||||
def mkFile(name: String): File = new File(tmpDir, name)
|
||||
|
||||
def writeFile(name: String, contents: String): Unit = {
|
||||
Files.write(mkFile(name).toPath, contents.getBytes): Unit
|
||||
}
|
||||
|
||||
def writeMain(contents: String): Unit = {
|
||||
Files.write(pkg.mainFile.toPath, contents.getBytes): Unit
|
||||
def writeMain(contents: String): File = {
|
||||
Files.write(pkg.mainFile.toPath, contents.getBytes).toFile
|
||||
}
|
||||
|
||||
def send(msg: Api.Request): Unit = endPoint.sendBinary(Api.serialize(msg))
|
||||
@ -79,49 +73,207 @@ class RuntimeServerTest
|
||||
}
|
||||
}
|
||||
|
||||
object Program {
|
||||
|
||||
val metadata = new Metadata
|
||||
|
||||
val idMainX = metadata.addItem(16, 5)
|
||||
val idMainY = metadata.addItem(30, 7)
|
||||
val idMainZ = metadata.addItem(46, 5)
|
||||
val idFooY = metadata.addItem(85, 8)
|
||||
val idFooZ = metadata.addItem(102, 5)
|
||||
|
||||
val text =
|
||||
"""
|
||||
|main =
|
||||
| x = 1 + 5
|
||||
| y = x.foo 5
|
||||
| z = y + 5
|
||||
| z
|
||||
|
|
||||
|Number.foo = x ->
|
||||
| y = this + 3
|
||||
| z = y * x
|
||||
| z
|
||||
|""".stripMargin
|
||||
|
||||
val code = metadata.appendToCode(text)
|
||||
|
||||
object update {
|
||||
|
||||
def idMainX(contextId: UUID) =
|
||||
Api.Response(
|
||||
Api.ExpressionValuesComputed(
|
||||
contextId,
|
||||
Vector(
|
||||
Api.ExpressionValueUpdate(
|
||||
Program.idMainX,
|
||||
Some("Number"),
|
||||
Some("6"),
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def idMainY(contextId: UUID) =
|
||||
Api.Response(
|
||||
Api.ExpressionValuesComputed(
|
||||
contextId,
|
||||
Vector(
|
||||
Api.ExpressionValueUpdate(
|
||||
Program.idMainY,
|
||||
Some("Number"),
|
||||
Some("45"),
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def idMainZ(contextId: UUID) =
|
||||
Api.Response(
|
||||
Api.ExpressionValuesComputed(
|
||||
contextId,
|
||||
Vector(
|
||||
Api.ExpressionValueUpdate(
|
||||
Program.idMainZ,
|
||||
Some("Number"),
|
||||
Some("50"),
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def idFooY(contextId: UUID) =
|
||||
Api.Response(
|
||||
Api.ExpressionValuesComputed(
|
||||
contextId,
|
||||
Vector(
|
||||
Api.ExpressionValueUpdate(
|
||||
Program.idFooY,
|
||||
Some("Number"),
|
||||
Some("9"),
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def idFooZ(contextId: UUID) =
|
||||
Api.Response(
|
||||
Api.ExpressionValuesComputed(
|
||||
contextId,
|
||||
Vector(
|
||||
Api.ExpressionValueUpdate(
|
||||
Program.idFooZ,
|
||||
Some("Number"),
|
||||
Some("45"),
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override protected def beforeEach(): Unit = {
|
||||
context = new TestContext("Test")
|
||||
val Some(Api.Response(_, Api.InitializedNotification())) = context.receive
|
||||
}
|
||||
|
||||
"Runtime server" should "allow executing a stack of functions by entering them through call-sites" in {
|
||||
val metadata = new Metadata
|
||||
val _ = metadata.addItem(14, 7)
|
||||
val idMainY = metadata.addItem(30, 7)
|
||||
val idFooY = metadata.addItem(85, 8)
|
||||
val idFooZ = metadata.addItem(102, 5)
|
||||
"RuntimeServer" should "push and pop functions on the stack" in {
|
||||
val mainFile = context.writeMain(Program.code)
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
|
||||
context.writeMain(
|
||||
metadata.appendToCode(
|
||||
"""
|
||||
|main =
|
||||
| x = 1 + 5
|
||||
| y = x.foo 5
|
||||
| z = y + 5
|
||||
| z
|
||||
|
|
||||
|Number.foo = x ->
|
||||
| y = this + 3
|
||||
| z = y * x
|
||||
| z
|
||||
|""".stripMargin
|
||||
)
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// push local item on top of the empty stack
|
||||
val invalidLocalItem = Api.StackItem.LocalCall(Program.idMainY)
|
||||
context.send(
|
||||
Api
|
||||
.Request(requestId, Api.PushContextRequest(contextId, invalidLocalItem))
|
||||
)
|
||||
Set.fill(2)(context.receive) shouldEqual Set(
|
||||
Some(Api.Response(requestId, Api.InvalidStackItemError(contextId))),
|
||||
None
|
||||
)
|
||||
|
||||
// push main
|
||||
val item1 = Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(mainFile, "Main", "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
|
||||
)
|
||||
Set.fill(5)(context.receive) shouldEqual Set(
|
||||
Some(Api.Response(requestId, Api.PushContextResponse(contextId))),
|
||||
Some(Program.update.idMainX(contextId)),
|
||||
Some(Program.update.idMainY(contextId)),
|
||||
Some(Program.update.idMainZ(contextId)),
|
||||
None
|
||||
)
|
||||
|
||||
// push foo call
|
||||
val item2 = Api.StackItem.LocalCall(Program.idMainY)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.PushContextRequest(contextId, item2))
|
||||
)
|
||||
Set.fill(4)(context.receive) shouldEqual Set(
|
||||
Some(Api.Response(requestId, Api.PushContextResponse(contextId))),
|
||||
Some(Program.update.idFooY(contextId)),
|
||||
Some(Program.update.idFooZ(contextId)),
|
||||
None
|
||||
)
|
||||
|
||||
// push method pointer on top of the non-empty stack
|
||||
val invalidExplicitCall = Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(mainFile, "Main", "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
context.send(
|
||||
Api.Request(
|
||||
UUID.randomUUID(),
|
||||
Api.Execute(
|
||||
"Test.Main",
|
||||
"Main",
|
||||
"main",
|
||||
List(idMainY)
|
||||
)
|
||||
requestId,
|
||||
Api.PushContextRequest(contextId, invalidExplicitCall)
|
||||
)
|
||||
)
|
||||
val updates = Set(context.receive, context.receive)
|
||||
updates shouldEqual Set(
|
||||
Some(Api.Response(Api.ExpressionValueUpdateNotification(idFooY, "9"))),
|
||||
Some(Api.Response(Api.ExpressionValueUpdateNotification(idFooZ, "45")))
|
||||
Set.fill(2)(context.receive) shouldEqual Set(
|
||||
Some(Api.Response(requestId, Api.InvalidStackItemError(contextId))),
|
||||
None
|
||||
)
|
||||
|
||||
// pop foo call
|
||||
context.send(Api.Request(requestId, Api.PopContextRequest(contextId)))
|
||||
Set.fill(5)(context.receive) shouldEqual Set(
|
||||
Some(Api.Response(requestId, Api.PopContextResponse(contextId))),
|
||||
Some(Program.update.idMainX(contextId)),
|
||||
Some(Program.update.idMainY(contextId)),
|
||||
Some(Program.update.idMainZ(contextId)),
|
||||
None
|
||||
)
|
||||
|
||||
// pop main
|
||||
context.send(Api.Request(requestId, Api.PopContextRequest(contextId)))
|
||||
Set.fill(2)(context.receive) shouldEqual Set(
|
||||
Some(Api.Response(requestId, Api.PopContextResponse(contextId))),
|
||||
None
|
||||
)
|
||||
|
||||
// pop empty stack
|
||||
context.send(Api.Request(requestId, Api.PopContextRequest(contextId)))
|
||||
Set.fill(2)(context.receive) shouldEqual Set(
|
||||
Some(Api.Response(requestId, Api.EmptyStackError(contextId))),
|
||||
None
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user