Populate the Suggestions Database with Imported Modules (#1155)

During the compilation, the runtime will analyze
all modules in scope and send the appropriate
suggestion updates to the server.
This commit is contained in:
Dmitry Bushev 2020-09-21 15:05:58 +03:00 committed by GitHub
parent 5cd977e904
commit 5ea7615bb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1087 additions and 213 deletions

View File

@ -553,7 +553,6 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
"commons-cli" % "commons-cli" % commonsCliVersion,
"commons-io" % "commons-io" % commonsIoVersion,
"com.beachape" %% "enumeratum-circe" % enumeratumCirceVersion,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion % Runtime,
"com.miguno.akka" %% "akka-mock-scheduler" % akkaMockSchedulerVersion % Test,
"org.mockito" %% "mockito-scala" % mockitoScalaVersion % Test
),
@ -691,7 +690,6 @@ lazy val searcher = project
libraryDependencies ++= jmh ++ Seq(
"com.typesafe.slick" %% "slick" % slickVersion,
"org.xerial" % "sqlite-jdbc" % sqliteVersion,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion % Runtime,
"ch.qos.logback" % "logback-classic" % logbackClassicVersion % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
)
@ -773,7 +771,6 @@ lazy val `language-server` = (project in file("engine/language-server"))
"com.google.flatbuffers" % "flatbuffers-java" % flatbuffersVersion,
"commons-io" % "commons-io" % commonsIoVersion,
akkaTestkit % Test,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion % Runtime,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.scalacheck" %% "scalacheck" % scalacheckVersion % Test,
"org.graalvm.sdk" % "polyglot-tck" % graalVersion % "provided"
@ -998,7 +995,6 @@ lazy val runner = project
"com.monovore" %% "decline" % declineVersion,
"org.jline" % "jline" % jlineVersion,
"org.typelevel" %% "cats-core" % catsVersion,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion % Runtime
),
connectInput in run := true
)

View File

@ -1,3 +1,3 @@
akka.http.server.idle-timeout = infinite
akka.http.server.remote-address-header = on
akka.http.server.websocket.periodic-keep-alive-max-idle = 1 second
akka.http.server.websocket.periodic-keep-alive-max-idle = 1 second

View File

@ -187,6 +187,10 @@ class MainModule(serverConfig: LanguageServerConfig) {
context.initialize(LanguageInfo.ID)
log.trace("Runtime context initialized")
val logLevel = Logging.LogLevel.fromJava(logHandler.getLevel)
system.eventStream.setLogLevel(Logging.LogLevel.toAkka(logLevel))
log.trace(s"Set akka log level to $logLevel")
val runtimeKiller =
system.actorOf(
RuntimeKiller.props(runtimeConnector, context),

View File

@ -12,6 +12,7 @@ import org.enso.languageserver.data.{
CapabilityRegistration,
ClientId,
Config,
ContentBasedVersioning,
ReceivesSuggestionsDatabaseUpdates
}
import org.enso.languageserver.event.InitializedEvent
@ -74,7 +75,8 @@ final class SuggestionsHandler(
fileVersionsRepo: FileVersionsRepo[Future],
sessionRouter: ActorRef,
runtimeConnector: ActorRef
) extends Actor
)(implicit versionCalculator: ContentBasedVersioning)
extends Actor
with Stash
with ActorLogging
with UnhandledLogging {
@ -91,6 +93,8 @@ final class SuggestionsHandler(
.subscribe(self, classOf[Api.SuggestionsDatabaseUpdateNotification])
context.system.eventStream
.subscribe(self, classOf[Api.SuggestionsDatabaseReIndexNotification])
context.system.eventStream
.subscribe(self, classOf[Api.SuggestionsDatabaseIndexUpdateNotification])
context.system.eventStream.subscribe(self, classOf[ProjectNameChangedEvent])
context.system.eventStream.subscribe(self, classOf[FileDeletedEvent])
context.system.eventStream
@ -139,6 +143,23 @@ final class SuggestionsHandler(
sender() ! CapabilityReleased
context.become(initialized(projectName, clients - client.clientId))
case msg: Api.SuggestionsDatabaseIndexUpdateNotification =>
applyIndexedModuleUpdates(msg.updates.toSeq)
.onComplete {
case Success(notification) =>
if (notification.updates.nonEmpty) {
clients.foreach { clientId =>
sessionRouter ! DeliverToJsonController(clientId, notification)
}
}
case Failure(ex) =>
log.error(
ex,
"Error applying suggestion database updates: {}",
msg.updates.map(_.file)
)
}
case msg: Api.SuggestionsDatabaseUpdateNotification =>
applyDatabaseUpdates(msg)
.onComplete {
@ -157,7 +178,8 @@ final class SuggestionsHandler(
}
case msg: Api.SuggestionsDatabaseReIndexNotification =>
applyReIndexUpdates(msg.moduleName, msg.updates)
log.debug(s"ReIndex ${msg.moduleName} ${msg.updates.map(_.suggestion)}")
applyReIndexUpdates(msg.updates)
.onComplete {
case Success(notification) =>
if (notification.updates.nonEmpty) {
@ -174,9 +196,11 @@ final class SuggestionsHandler(
}
case Api.ExpressionValuesComputed(_, updates) =>
val types = updates.flatMap(update =>
update.expressionType.map(update.expressionId -> _)
log.debug(
s"ExpressionValuesComputed ${updates.map(u => (u.expressionId, u.expressionType))}"
)
val types = updates
.flatMap(update => update.expressionType.map(update.expressionId -> _))
suggestionsRepo
.updateAll(types)
.map {
@ -290,6 +314,33 @@ final class SuggestionsHandler(
}
}
private def applyIndexedModuleUpdates(
updates: Seq[Api.IndexedModule]
): Future[SuggestionsDatabaseUpdateNotification] = {
def createIndexedModuleUpdatesBatch(
contents: String,
file: java.io.File,
updates: Seq[Api.SuggestionsDatabaseUpdate.Add]
): Future[Seq[Api.SuggestionsDatabaseUpdate.Add]] =
fileVersionsRepo
.updateVersion(file, versionCalculator.evalDigest(contents))
.map(versionChanged => if (versionChanged) updates else Seq())
def getBatches =
Future
.traverse(updates) { indexed =>
createIndexedModuleUpdatesBatch(
indexed.contents,
indexed.file,
indexed.updates
)
}
.map(_.flatten)
for {
batch <- getBatches
update <- applyReIndexUpdates(batch)
} yield update
}
/**
* Handle the suggestions database re-index update.
*
@ -297,17 +348,17 @@ final class SuggestionsHandler(
* suggestions and builds the notification containing combined removed and
* added suggestions.
*
* @param moduleName the module name
* @param updates the list of updates after the full module re-index
* @return the API suggestions database update notification
*/
private def applyReIndexUpdates(
moduleName: String,
updates: Seq[Api.SuggestionsDatabaseUpdate.Add]
): Future[SuggestionsDatabaseUpdateNotification] = {
val added = updates.map(_.suggestion)
val added = updates.map(_.suggestion)
val modules = updates.map(_.suggestion.module).distinct
log.debug(s"Applying re-index updates; modules=$modules")
for {
(_, removedIds) <- suggestionsRepo.removeByModule(moduleName)
(_, removedIds) <- suggestionsRepo.removeAllByModule(modules)
(version, addedIds) <- suggestionsRepo.insertAll(added)
} yield {
val updatesRemoved = removedIds.map(SuggestionsDatabaseUpdate.Remove)
@ -315,7 +366,7 @@ final class SuggestionsHandler(
case (Some(id), suggestion) =>
Some(SuggestionsDatabaseUpdate.Add(id, suggestion))
case (None, suggestion) =>
log.error("failed to insert suggestion: {}", suggestion)
log.error("Failed to insert re-index suggestion: {}", suggestion)
None
}
SuggestionsDatabaseUpdateNotification(
@ -344,7 +395,10 @@ final class SuggestionsHandler(
case ((add, remove), msg: Api.SuggestionsDatabaseUpdate.Remove) =>
(add, remove :+ msg.suggestion)
}
log.debug(
s"Applying suggestion updates; added=${added
.map(_.name)}; removed=${removed.map(_.name)}"
)
for {
(_, removedIds) <- suggestionsRepo.removeAll(removed)
(version, addedIds) <- suggestionsRepo.insertAll(added)
@ -357,7 +411,7 @@ final class SuggestionsHandler(
case (Some(id), suggestion) =>
Some(SuggestionsDatabaseUpdate.Add(id, suggestion))
case (None, suggestion) =>
log.error("failed to insert suggestion: {}", suggestion)
log.error("Failed to insert suggestion: {}", suggestion)
None
}
SuggestionsDatabaseUpdateNotification(
@ -437,7 +491,7 @@ object SuggestionsHandler {
fileVersionsRepo: FileVersionsRepo[Future],
sessionRouter: ActorRef,
runtimeConnector: ActorRef
): Props =
)(implicit versionCalculator: ContentBasedVersioning): Props =
Props(
new SuggestionsHandler(
config,

View File

@ -70,6 +70,16 @@ object Logging {
case Debug => event.Level.DEBUG
case Trace => event.Level.TRACE
}
/** Convert to akka logging level. */
def toAkka(level: LogLevel): akka.event.Logging.LogLevel =
level match {
case Error => akka.event.Logging.ErrorLevel
case Warning => akka.event.Logging.WarningLevel
case Info => akka.event.Logging.InfoLevel
case Debug => akka.event.Logging.DebugLevel
case Trace => akka.event.Logging.DebugLevel
}
}
private val ROOT_LOGGER = "org.enso"

View File

@ -242,7 +242,7 @@ class SuggestionsHandlerSpec
runtimeConnector: TestProbe,
suggestionsRepo: SuggestionsRepo[Future],
fileVersionsRepo: FileVersionsRepo[Future]
): ActorRef = {
)(implicit versionCalculator: ContentBasedVersioning): ActorRef = {
val handler =
system.actorOf(
SuggestionsHandler.props(
@ -284,7 +284,8 @@ class SuggestionsHandlerSpec
ActorRef
) => Any
): Unit = {
val testContentRoot = Files.createTempDirectory(null).toRealPath()
implicit val versionCalc = Sha3_224VersionCalculator
val testContentRoot = Files.createTempDirectory(null).toRealPath()
sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile))
val config = newConfig(testContentRoot.toFile)
val router = TestProbe("session-router")

View File

@ -80,6 +80,8 @@ class BaseServerTest extends JsonRpcServerTestKit {
)
override def clientControllerFactory: ClientControllerFactory = {
implicit val versionCalculator = Sha3_224VersionCalculator
val zioExec = ZioExec(zio.Runtime.default)
val sqlDatabase = SqlDatabase(config.directories.suggestionsDatabaseFile)
val suggestionsRepo = new SqlSuggestionsRepo(sqlDatabase)(system.dispatcher)

View File

@ -26,7 +26,12 @@ import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
)
)
)
sealed trait Suggestion
sealed trait Suggestion {
def name: String
def module: String
}
object Suggestion {
type ExternalId = UUID

View File

@ -179,6 +179,10 @@ object Runtime {
new JsonSubTypes.Type(
value = classOf[Api.InvalidateModulesIndexResponse],
name = "invalidateModulesIndexResponse"
),
new JsonSubTypes.Type(
value = classOf[Api.SuggestionsDatabaseIndexUpdateNotification],
name = "suggestionsDatabaseIndexUpdateNotification"
)
)
)
@ -726,18 +730,40 @@ object Runtime {
updates: Seq[SuggestionsDatabaseUpdate.Add]
) extends ApiNotification
private lazy val mapper = {
val factory = new CBORFactory()
val mapper = new ObjectMapper(factory) with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
}
/** A request to invalidate the indexed flag of the modules. */
case class InvalidateModulesIndexRequest() extends ApiRequest
/** Signals that the module indexes has been invalidated. */
case class InvalidateModulesIndexResponse() extends ApiResponse
/**
* An indexed module.
*
* @param file the module file path
* @param contents the module source
* @param updates the list of suggestions extracted from module
*/
case class IndexedModule(
file: File,
contents: String,
updates: Seq[SuggestionsDatabaseUpdate.Add]
) extends ApiNotification
/**
* A notification about new indexed modules.
*
* @param updates the list of suggestions database updates
*/
case class SuggestionsDatabaseIndexUpdateNotification(
updates: Iterable[IndexedModule]
) extends ApiNotification
private lazy val mapper = {
val factory = new CBORFactory()
val mapper = new ObjectMapper(factory) with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
}
/**
* Serializes a Request into a byte buffer.
*

View File

@ -2,7 +2,6 @@ package org.enso.interpreter;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLanguage;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

View File

@ -9,7 +9,6 @@ import org.enso.interpreter.Language;
import org.enso.interpreter.OptionsHelper;
import org.enso.interpreter.runtime.builtin.Builtins;
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
import org.enso.interpreter.runtime.scope.ModuleScope;
import org.enso.interpreter.runtime.scope.TopLevelScope;
import org.enso.interpreter.runtime.util.TruffleFileSystem;
import org.enso.interpreter.util.ScalaConversions;

View File

@ -229,6 +229,7 @@ public class Module implements TruffleObject {
Source.newBuilder(LanguageInfo.ID, literalSource.characters(), name.toString()).build();
} else if (sourceFile != null) {
cachedSource = Source.newBuilder(LanguageInfo.ID, sourceFile).build();
literalSource = Rope.apply(cachedSource.getCharacters().toString());
}
return cachedSource;
}
@ -345,6 +346,7 @@ public class Module implements TruffleObject {
throws ArityException {
Types.extractArguments(args);
module.cachedSource = null;
module.literalSource = null;
try {
module.compile(context);
} catch (IOException ignored) {

View File

@ -5,7 +5,7 @@ import com.oracle.truffle.api.instrumentation.EventBinding;
import com.oracle.truffle.api.instrumentation.ExecutionEventListener;
import com.oracle.truffle.api.interop.*;
import com.oracle.truffle.api.source.SourceSection;
import org.enso.compiler.context.Changeset;
import org.enso.compiler.context.ChangesetBuilder;
import org.enso.interpreter.instrument.IdExecutionInstrument;
import org.enso.interpreter.instrument.MethodCallsCache;
import org.enso.interpreter.instrument.RuntimeCache;
@ -16,10 +16,7 @@ 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.interpreter.runtime.state.data.EmptyMap;
import org.enso.interpreter.service.error.ConstructorNotFoundException;
import org.enso.interpreter.service.error.MethodNotFoundException;
import org.enso.interpreter.service.error.ModuleNotFoundException;
import org.enso.interpreter.service.error.SourceNotFoundException;
import org.enso.interpreter.service.error.*;
import org.enso.polyglot.LanguageInfo;
import org.enso.polyglot.MethodNames;
import org.enso.text.buffer.Rope;
@ -29,6 +26,7 @@ import org.enso.text.editing.TextEditor;
import org.enso.text.editing.model;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -204,7 +202,7 @@ public class ExecutionService {
*/
public void setModuleSources(File path, String contents, boolean isIndexed) {
Optional<Module> module = context.getModuleForFile(path);
if (!module.isPresent()) {
if (module.isEmpty()) {
module = context.createModuleForFile(path);
}
module.ifPresent(
@ -241,23 +239,30 @@ public class ExecutionService {
* @param edits the edits to apply.
* @return an object for computing the changed IR nodes.
*/
public Optional<Changeset<Rope>> modifyModuleSources(File path, List<model.TextEdit> edits) {
public ChangesetBuilder<Rope> modifyModuleSources(
File path, List<model.TextEdit> edits) {
Optional<Module> moduleMay = context.getModuleForFile(path);
if (!moduleMay.isPresent()) {
return Optional.empty();
if (moduleMay.isEmpty()) {
throw new ModuleNotFoundForFileException(path);
}
Module module = moduleMay.get();
if (module.getLiteralSource() == null) {
return Optional.empty();
try {
module.getSource();
} catch (IOException e) {
throw new SourceNotFoundException(path, e);
}
Changeset<Rope> changeset =
new Changeset<>(
ChangesetBuilder<Rope> changesetBuilder =
new ChangesetBuilder<>(
module.getLiteralSource(),
module.getIr(),
TextEditor.ropeTextEditor(),
IndexedSource.RopeIndexedSource());
Optional<Rope> editedSource = JavaEditorAdapter.applyEdits(module.getLiteralSource(), edits);
editedSource.ifPresent(module::setLiteralSource);
return Optional.of(changeset);
editedSource.ifPresentOrElse(
module::setLiteralSource,
() -> {
throw new FailedToApplyEditsException(path);
});
return changesetBuilder;
}
}

View File

@ -0,0 +1,16 @@
package org.enso.interpreter.service.error;
import java.io.File;
/** Thrown when the edits can not be applied to the source. */
public class FailedToApplyEditsException extends RuntimeException implements ServiceException {
/**
* Create new instance of this error.
*
* @param path the source file path.
*/
public FailedToApplyEditsException(File path) {
super("Filed to apply edits for file " + path + ".");
}
}

View File

@ -0,0 +1,17 @@
package org.enso.interpreter.service.error;
import java.io.File;
/** Thrown when a module for given file was requested but could not be found. */
public class ModuleNotFoundForFileException extends ModuleNotFoundException {
/**
* Create new instance of this error.
*
* @param path the path of the module file.
*/
public ModuleNotFoundForFileException(File path) {
super("Module not found for file " + path + ".");
}
}

View File

@ -8,7 +8,17 @@ public class SourceNotFoundException extends RuntimeException implements Service
*
* @param item the item which source code is missing.
*/
public SourceNotFoundException(String item) {
public SourceNotFoundException(Object item) {
super("The " + item + " source not found.");
}
/**
* Create new instance of this error.
*
* @param item the item which source code is missing.
* @param cause the cause of the exception.
*/
public SourceNotFoundException(Object item, Throwable cause) {
super("The " + item + " source not found.", cause);
}
}

View File

@ -11,13 +11,39 @@ import org.enso.text.editing.{IndexedSource, TextEditor}
import scala.collection.mutable
/** The changeset of a module containing the computed list of invalidated
* expressions.
*
* @param source the module source
* @param ir the IR node of the module
* @param invalidated the list of invalidated expressions
* @tparam A the source type
*/
case class Changeset[A](
source: A,
ir: IR,
invalidated: Set[IR.ExternalId]
)
/** Compute invalidated expressions.
*
* @param source the text source
* @param ir the IR node
* @tparam A the source type
*/
final class Changeset[A: TextEditor: IndexedSource](val source: A, val ir: IR) {
final class ChangesetBuilder[A: TextEditor: IndexedSource](
val source: A,
val ir: IR
) {
/** Build the changeset containing the nodes invalidated by the edits.
*
* @param edits the edits applied to the source
* @return the computed changeset
*/
@throws[CompilerError]
def build(edits: Seq[TextEdit]): Changeset[A] =
Changeset(source, ir, compute(edits))
/** Traverses the IR and returns a list of all IR nodes affected by the edit
* using the [[DataflowAnalysis]] information.
@ -35,7 +61,7 @@ final class Changeset[A: TextEditor: IndexedSource](val source: A, val ir: IR) {
)
val direct = invalidated(edits)
val transitive = direct
.map(Changeset.toDataflowDependencyType)
.map(ChangesetBuilder.toDataflowDependencyType)
.flatMap(metadata.getExternal)
.flatten
direct.flatMap(_.externalId) ++ transitive
@ -47,25 +73,26 @@ final class Changeset[A: TextEditor: IndexedSource](val source: A, val ir: IR) {
* @param edits the text edits
* @return the set of IR nodes directly affected by the edit
*/
def invalidated(edits: Seq[TextEdit]): Set[Changeset.NodeId] = {
def invalidated(edits: Seq[TextEdit]): Set[ChangesetBuilder.NodeId] = {
@scala.annotation.tailrec
def go(
tree: Changeset.Tree,
tree: ChangesetBuilder.Tree,
source: A,
edits: mutable.Queue[TextEdit],
ids: mutable.Set[Changeset.NodeId]
): Set[Changeset.NodeId] = {
ids: mutable.Set[ChangesetBuilder.NodeId]
): Set[ChangesetBuilder.NodeId] = {
if (edits.isEmpty) ids.toSet
else {
val edit = edits.dequeue()
val locationEdit = Changeset.toLocationEdit(edit, source)
val invalidatedSet = Changeset.invalidated(tree, locationEdit.location)
val newTree = Changeset.updateLocations(tree, locationEdit)
val newSource = TextEditor[A].edit(source, edit)
val edit = edits.dequeue()
val locationEdit = ChangesetBuilder.toLocationEdit(edit, source)
val invalidatedSet =
ChangesetBuilder.invalidated(tree, locationEdit.location)
val newTree = ChangesetBuilder.updateLocations(tree, locationEdit)
val newSource = TextEditor[A].edit(source, edit)
go(newTree, newSource, edits, ids ++= invalidatedSet.map(_.id))
}
}
val tree = Changeset.buildTree(ir)
val tree = ChangesetBuilder.buildTree(ir)
go(tree, source, mutable.Queue.from(edits), mutable.HashSet())
}
@ -79,7 +106,7 @@ final class Changeset[A: TextEditor: IndexedSource](val source: A, val ir: IR) {
}
object Changeset {
object ChangesetBuilder {
/** An identifier of IR node.
*
@ -226,7 +253,7 @@ object Changeset {
* @return the invalidated nodes of the tree
*/
private def invalidated(tree: Tree, edit: Location): Tree = {
val invalidated = mutable.TreeSet[Changeset.Node]()
val invalidated = mutable.TreeSet[ChangesetBuilder.Node]()
tree.iterator.foreach { node =>
if (intersect(edit, node)) {
invalidated += node
@ -242,7 +269,10 @@ object Changeset {
* @param node the node
* @return true if the node and edit locations are intersecting
*/
private def intersect(edit: Location, node: Changeset.Node): Boolean = {
private def intersect(
edit: Location,
node: ChangesetBuilder.Node
): Boolean = {
intersect(edit, node.location)
}
@ -268,7 +298,7 @@ object Changeset {
private def inside(index: Int, location: Location): Boolean =
index >= location.start && index <= location.end
/** Convert [[TextEdit]] to [[Changeset.LocationEdit]] edit in the provided
/** Convert [[TextEdit]] to [[ChangesetBuilder.LocationEdit]] edit in the provided
* source.
*
* @param edit the text edit

View File

@ -72,7 +72,6 @@ final class Handler {
): Unit = {
executionService = service
truffleContext = context
endpoint.sendToClient(Api.Response(Api.InitializedNotification()))
val interpreterCtx =
InterpreterContext(
executionService,
@ -81,6 +80,7 @@ final class Handler {
truffleContext
)
commandProcessor = new CommandExecutionEngine(interpreterCtx)
endpoint.sendToClient(Api.Response(Api.InitializedNotification()))
}
/**

View File

@ -25,7 +25,7 @@ class EditFileCmd(request: Api.EditFileNotification) extends Command(None) {
for {
_ <- Future { ctx.jobControlPlane.abortAllJobs() }
_ <- ctx.jobProcessor.run(
new EnsureCompiledJob(request.path, request.edits)
EnsureCompiledJob(request.path, request.edits)
)
_ <- Future.sequence(executeJobs.map(ctx.jobProcessor.run))
} yield ()

View File

@ -2,7 +2,6 @@ package org.enso.interpreter.instrument.command
import org.enso.interpreter.instrument.execution.RuntimeContext
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.polyglot.runtime.Runtime.Api.RequestId
import scala.concurrent.{ExecutionContext, Future}
@ -13,7 +12,7 @@ import scala.concurrent.{ExecutionContext, Future}
* @param request a request for invalidation
*/
class InvalidateModulesIndexCmd(
maybeRequestId: Option[RequestId],
maybeRequestId: Option[Api.RequestId],
val request: Api.InvalidateModulesIndexRequest
) extends Command(maybeRequestId) {

View File

@ -1,9 +1,10 @@
package org.enso.interpreter.instrument.job
import java.io.File
import java.util.Optional
import java.io.{File, IOException}
import java.util.logging.Level
import org.enso.compiler.context.{Changeset, SuggestionBuilder}
import org.enso.compiler.phase.ImportResolver
import org.enso.interpreter.instrument.CacheInvalidation
import org.enso.interpreter.instrument.execution.RuntimeContext
import org.enso.interpreter.runtime.Module
@ -24,63 +25,158 @@ import scala.jdk.OptionConverters._
class EnsureCompiledJob(protected val files: Iterable[File])
extends Job[Unit](List.empty, true, false) {
/**
* Create a job ensuring that files are compiled after applying the edits.
*
* @param file a file to compile
*/
def this(file: File, edits: Seq[TextEdit]) = {
this(List(file))
EnsureCompiledJob.enqueueEdits(file, edits)
}
/** @inheritdoc */
override def run(implicit ctx: RuntimeContext): Unit = {
ctx.locking.acquireWriteCompilationLock()
try {
ensureCompiled(files)
val modules = ensureCompiledFiles(files)
ensureCompiledScope()
ensureCompiledImports(modules)
} finally {
ctx.locking.releaseWriteCompilationLock()
}
}
/**
* Run the compilation and invalidation logic.
* Run the scheduled compilation and invalidation logic.
*
* @param files the list of files to compile
* @param files the list of files to compile.
* @param ctx the runtime context
*/
protected def ensureCompiled(
protected def ensureCompiledFiles(
files: Iterable[File]
)(implicit ctx: RuntimeContext): Iterable[Module] = {
for {
file <- files
module <- compile(file)
} yield {
val changeset = applyEdits(file)
compile(module)
runInvalidationCommands(
buildCacheInvalidationCommands(changeset, module.getLiteralSource)
)
analyzeModule(module, changeset)
module
}
}
/**
* Compile the imported modules and send the suggestion updates.
*
* @param modules the list of modules to analyze.
* @param ctx the runtime context
*/
protected def ensureCompiledImports(
modules: Iterable[Module]
)(implicit ctx: RuntimeContext): Unit = {
files.foreach { file =>
compile(file).foreach { module =>
applyEdits(file).ifPresent {
case (changeset, edits) =>
val moduleName = module.getName.toString
runInvalidationCommands(
buildCacheInvalidationCommands(changeset, edits)
)
if (module.isIndexed) {
val removedSuggestions = SuggestionBuilder(changeset.source)
.build(moduleName, module.getIr)
compile(module)
val addedSuggestions =
SuggestionBuilder(changeset.applyEdits(edits))
.build(moduleName, module.getIr)
sendSuggestionsUpdateNotification(
removedSuggestions diff addedSuggestions,
addedSuggestions diff removedSuggestions
)
} else {
val addedSuggestions =
SuggestionBuilder(changeset.applyEdits(edits))
.build(moduleName, module.getIr)
sendReIndexNotification(moduleName, addedSuggestions)
module.setIndexed(true)
}
}
}
modules.foreach { module =>
compile(module)
val importedModules =
new ImportResolver(ctx.executionService.getContext.getCompiler)
.mapImports(module)
.filter(_.getName != module.getName)
ctx.executionService.getLogger.finest(
s"Module ${module.getName} imports ${importedModules.map(_.getName)}"
)
importedModules.foreach(analyzeImport)
}
}
/**
* Compile all modules in the scope and send the extracted suggestions.
*
* @param ctx the runtime context
*/
protected def ensureCompiledScope()(implicit ctx: RuntimeContext): Unit = {
val modulesInScope =
ctx.executionService.getContext.getTopScope.getModules.asScala
ctx.executionService.getLogger
.finest(s"Modules in scope: ${modulesInScope.map(_.getName)}")
val updates = modulesInScope.flatMap { module =>
compile(module)
analyzeModuleInScope(module)
}
sendIndexUpdateNotification(
Api.SuggestionsDatabaseIndexUpdateNotification(updates)
)
}
private def analyzeImport(
module: Module
)(implicit ctx: RuntimeContext): Unit = {
if (!module.isIndexed && module.getLiteralSource != null) {
ctx.executionService.getLogger
.finest(s"Analyzing imported ${module.getName}")
val moduleName = module.getName.toString
val addedSuggestions = SuggestionBuilder(module.getLiteralSource)
.build(module.getName.toString, module.getIr)
.filter(isSuggestionGlobal)
sendReIndexNotification(moduleName, addedSuggestions)
module.setIndexed(true)
}
}
private def analyzeModuleInScope(module: Module)(implicit
ctx: RuntimeContext
): Option[Api.IndexedModule] = {
try module.getSource
catch {
case e: IOException =>
ctx.executionService.getLogger.log(
Level.SEVERE,
s"Failed to get module source to analyze ${module.getName}",
e
)
}
if (
!module.isIndexed &&
module.getLiteralSource != null &&
module.getPath != null
) {
ctx.executionService.getLogger
.finest(s"Analyzing module in scope ${module.getName}")
val moduleName = module.getName.toString
val addedSuggestions = SuggestionBuilder(module.getLiteralSource)
.build(moduleName, module.getIr)
.filter(isSuggestionGlobal)
module.setIndexed(true)
Some(
Api.IndexedModule(
new File(module.getPath),
module.getSource.getCharacters.toString,
addedSuggestions.map(Api.SuggestionsDatabaseUpdate.Add)
)
)
} else {
None
}
}
private def analyzeModule(
module: Module,
changeset: Changeset[Rope]
)(implicit ctx: RuntimeContext): Unit = {
val moduleName = module.getName.toString
if (module.isIndexed) {
ctx.executionService.getLogger
.finest(s"Analyzing indexed module ${module.getName}")
val removedSuggestions = SuggestionBuilder(changeset.source)
.build(moduleName, changeset.ir)
val addedSuggestions =
SuggestionBuilder(module.getLiteralSource)
.build(moduleName, module.getIr)
sendSuggestionsUpdateNotification(
removedSuggestions diff addedSuggestions,
addedSuggestions diff removedSuggestions
)
} else {
ctx.executionService.getLogger
.finest(s"Analyzing not-indexed module ${module.getName}")
val addedSuggestions =
SuggestionBuilder(module.getLiteralSource)
.build(moduleName, module.getIr)
sendReIndexNotification(moduleName, addedSuggestions)
module.setIndexed(true)
}
}
@ -107,28 +203,36 @@ class EnsureCompiledJob(protected val files: Iterable[File])
* @param ctx the runtime context
* @return the compiled module
*/
private def compile(module: Module)(implicit ctx: RuntimeContext): Module =
private def compile(
module: Module
)(implicit ctx: RuntimeContext): Module = {
val prevStage = module.getCompilationStage
module.compileScope(ctx.executionService.getContext).getModule
if (prevStage != module.getCompilationStage) {
ctx.executionService.getLogger.finer(
s"Compiled ${module.getName} $prevStage->${module.getCompilationStage}"
)
}
module
}
/**
* Apply pending edits to the file.
*
* @param file the file to apply edits to
* @param ctx the runtime context
* @return the [[Changeset]] object and the list of applied edits
* @return the [[Changeset]] after applying the edits to the source
*/
private def applyEdits(
file: File
)(implicit
ctx: RuntimeContext
): Optional[(Changeset[Rope], Seq[TextEdit])] = {
)(implicit ctx: RuntimeContext): Changeset[Rope] = {
ctx.locking.acquireFileLock(file)
ctx.locking.acquireReadCompilationLock()
try {
val edits = EnsureCompiledJob.dequeueEdits(file)
ctx.executionService
val suggestionBuilder = ctx.executionService
.modifyModuleSources(file, edits.asJava)
.map(_ -> edits)
suggestionBuilder.build(edits)
} finally {
ctx.locking.releaseReadCompilationLock()
ctx.locking.releaseFileLock(file)
@ -138,20 +242,19 @@ class EnsureCompiledJob(protected val files: Iterable[File])
/**
* Create cache invalidation commands after applying the edits.
*
* @param changeset the [[Changeset]] object capturing the previous version
* of IR
* @param edits the list of applied edits
* @param changeset the [[Changeset]] object capturing the previous
* version of IR
* @param ctx the runtime context
* @return the list of cache invalidation commands
*/
private def buildCacheInvalidationCommands(
changeset: Changeset[Rope],
edits: Seq[TextEdit]
source: Rope
)(implicit ctx: RuntimeContext): Seq[CacheInvalidation] = {
val invalidateExpressionsCommand =
CacheInvalidation.Command.InvalidateKeys(changeset.compute(edits))
CacheInvalidation.Command.InvalidateKeys(changeset.invalidated)
val scopeIds = ctx.executionService.getContext.getCompiler
.parseMeta(changeset.source.toString)
.parseMeta(source.toString)
.map(_._2)
val invalidateStaleCommand =
CacheInvalidation.Command.InvalidateStale(scopeIds)
@ -217,7 +320,7 @@ class EnsureCompiledJob(protected val files: Iterable[File])
private def sendReIndexNotification(
moduleName: String,
added: Seq[Suggestion]
)(implicit ctx: RuntimeContext): Unit =
)(implicit ctx: RuntimeContext): Unit = {
ctx.endpoint.sendToClient(
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
@ -226,6 +329,25 @@ class EnsureCompiledJob(protected val files: Iterable[File])
)
)
)
}
private def sendIndexUpdateNotification(
msg: Api.SuggestionsDatabaseIndexUpdateNotification
)(implicit
ctx: RuntimeContext
): Unit = {
if (msg.updates.nonEmpty) {
ctx.endpoint.sendToClient(Api.Response(msg))
}
}
private def isSuggestionGlobal(suggestion: Suggestion): Boolean =
suggestion match {
case _: Suggestion.Atom => true
case _: Suggestion.Method => true
case _: Suggestion.Function => false
case _: Suggestion.Local => false
}
}
object EnsureCompiledJob {
@ -241,4 +363,46 @@ object EnsureCompiledJob {
case Some(v) => Some(v :++ edits)
case None => Some(edits)
}
/**
* Create a job ensuring that files are compiled.
*
* @param files the list of files to compile
* @return a new job
*/
def apply(files: Iterable[File]): EnsureCompiledJob = {
new EnsureCompiledJob(files)
}
/**
* Create a job ensuring that files are compiled after applying the edits.
*
* @param file a file to compile
* @param edits the list of edits to apply
* @return a new job
*/
def apply(file: File, edits: Seq[TextEdit]): EnsureCompiledJob = {
EnsureCompiledJob.enqueueEdits(file, edits)
EnsureCompiledJob(List(file))
}
/**
* Create a job ensuring that modules are compiled.
*
* @param modules a list of modules to compile
* @return a new job
*/
def apply(
modules: Iterable[Module]
)(implicit ctx: RuntimeContext): EnsureCompiledJob = {
val files = modules.flatMap { module =>
val file = Option(module.getPath).map(new File(_))
if (file.isEmpty) {
ctx.executionService.getLogger
.severe(s"Failed to get file for module ${module.getName}")
}
file
}
new EnsureCompiledJob(files)
}
}

View File

@ -5,6 +5,7 @@ import java.io.File
import org.enso.compiler.pass.analyse.CachePreferenceAnalysis
import org.enso.interpreter.instrument.{CacheInvalidation, InstrumentFrame}
import org.enso.interpreter.instrument.execution.RuntimeContext
import org.enso.interpreter.runtime.Module
import org.enso.polyglot.runtime.Runtime.Api
import scala.jdk.OptionConverters._
@ -19,10 +20,10 @@ class EnsureCompiledStackJob(stack: Iterable[InstrumentFrame])(implicit
) extends EnsureCompiledJob(EnsureCompiledStackJob.extractFiles(stack)) {
/** @inheritdoc */
override def ensureCompiled(
override protected def ensureCompiledFiles(
files: Iterable[File]
)(implicit ctx: RuntimeContext): Unit = {
super.ensureCompiled(files)
)(implicit ctx: RuntimeContext): Iterable[Module] = {
val modules = super.ensureCompiledFiles(files)
getCacheMetadata(stack).foreach { metadata =>
CacheInvalidation.run(
stack,
@ -32,6 +33,7 @@ class EnsureCompiledStackJob(stack: Iterable[InstrumentFrame])(implicit
)
)
}
modules
}
private def getCacheMetadata(
@ -44,7 +46,7 @@ class EnsureCompiledStackJob(stack: Iterable[InstrumentFrame])(implicit
module.getIr
.unsafeGetMetadata(
CachePreferenceAnalysis,
"Empty cache preference metadata"
s"Empty cache preference metadata ${module.getName}"
)
}
case _ => None
@ -68,7 +70,14 @@ object EnsureCompiledStackJob {
case Api.StackItem.ExplicitCall(methodPointer, _, _) =>
ctx.executionService.getContext
.findModule(methodPointer.module)
.flatMap(module => java.util.Optional.ofNullable(module.getPath))
.flatMap { module =>
val path = java.util.Optional.ofNullable(module.getPath)
if (path.isEmpty) {
ctx.executionService.getLogger
.severe(s"${module.getName} module path is empty")
}
path
}
.map(path => new File(path))
.toScala
case _ =>

View File

@ -1,12 +1,12 @@
package org.enso.compiler.test.context
import org.enso.compiler.context.Changeset
import org.enso.compiler.context.ChangesetBuilder
import org.enso.compiler.core.IR
import org.enso.compiler.test.CompilerTest
import org.enso.text.buffer.Rope
import org.enso.text.editing.model.{Position, Range, TextEdit}
class ChangesetTest extends CompilerTest {
class ChangesetBuilderTest extends CompilerTest {
"DiffChangeset" should {
@ -213,5 +213,5 @@ class ChangesetTest extends CompilerTest {
}
def invalidated(ir: IR, code: String, edits: TextEdit*): Set[IR.Identifier] =
new Changeset(Rope(code), ir).invalidated(edits).map(_.internalId)
new ChangesetBuilder(Rope(code), ir).invalidated(edits).map(_.internalId)
}

View File

@ -283,7 +283,8 @@ class RuntimeServerTest
}
"RuntimeServer" should "push and pop functions on the stack" in {
context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -293,6 +294,12 @@ class RuntimeServerTest
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// open file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, true))
)
context.receiveNone shouldEqual None
// push local item on top of the empty stack
val invalidLocalItem = Api.StackItem.LocalCall(context.Main.idMainY)
context.send(
@ -1861,8 +1868,7 @@ class RuntimeServerTest
)
}
it should "support file modification operations" in {
val fooFile = new File(context.pkg.sourceDir, "Foo.enso")
it should "support file modification operations without attached ids" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -1878,18 +1884,10 @@ class RuntimeServerTest
|""".stripMargin
// Create a new file
context.writeFile(fooFile, code)
val mainFile = context.writeMain(code)
// Open the new file
context.send(
Api.Request(
Api.OpenFileNotification(
fooFile,
code,
false
)
)
)
context.send(Api.Request(Api.OpenFileNotification(mainFile, code, false)))
context.receiveNone shouldEqual None
context.consumeOut shouldEqual List()
@ -1901,7 +1899,7 @@ class RuntimeServerTest
contextId,
Api.StackItem
.ExplicitCall(
Api.MethodPointer("Test.Foo", "Foo", "main"),
Api.MethodPointer("Test.Main", "Main", "main"),
None,
Vector()
)
@ -1912,15 +1910,15 @@ class RuntimeServerTest
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
"Test.Foo",
"Test.Main",
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Foo",
"Test.Main",
"main",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"Foo",
"Main",
"Any",
None
)
@ -1936,7 +1934,7 @@ class RuntimeServerTest
context.send(
Api.Request(
Api.EditFileNotification(
fooFile,
mainFile,
Seq(
TextEdit(
model.Range(model.Position(2, 25), model.Position(2, 29)),
@ -1950,12 +1948,11 @@ class RuntimeServerTest
context.consumeOut shouldEqual List("I'm a modified!")
// Close the file
context.send(Api.Request(Api.CloseFileNotification(fooFile)))
context.send(Api.Request(Api.CloseFileNotification(mainFile)))
context.consumeOut shouldEqual List()
}
it should "support file modification operations with attached ids" in {
val fooFile = new File(context.pkg.sourceDir, "Foo.enso")
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val metadata = new Metadata
@ -1968,13 +1965,13 @@ class RuntimeServerTest
)
// Create a new file
context.writeFile(fooFile, code)
val mainFile = context.writeMain(code)
// Open the new file
context.send(
Api.Request(
Api.OpenFileNotification(
fooFile,
mainFile,
code,
false
)
@ -1990,7 +1987,7 @@ class RuntimeServerTest
contextId,
Api.StackItem
.ExplicitCall(
Api.MethodPointer("Test.Foo", "Foo", "main"),
Api.MethodPointer("Test.Main", "Main", "main"),
None,
Vector()
)
@ -2009,15 +2006,15 @@ class RuntimeServerTest
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
"Test.Foo",
"Test.Main",
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
"Test.Foo",
"Test.Main",
"main",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"Foo",
"Main",
"Any",
None
)
@ -2032,7 +2029,7 @@ class RuntimeServerTest
context.send(
Api.Request(
Api.EditFileNotification(
fooFile,
mainFile,
Seq(
TextEdit(
model.Range(model.Position(0, 0), model.Position(0, 9)),
@ -2222,7 +2219,6 @@ class RuntimeServerTest
}
it should "send suggestion notifications when file is modified" in {
val fooFile = new File(context.pkg.sourceDir, "Foo.enso")
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -2238,13 +2234,13 @@ class RuntimeServerTest
|""".stripMargin
// Create a new file
context.writeFile(fooFile, code)
val mainFile = context.writeMain(code)
// Open the new file
context.send(
Api.Request(
Api.OpenFileNotification(
fooFile,
mainFile,
code,
false
)
@ -2261,7 +2257,7 @@ class RuntimeServerTest
contextId,
Api.StackItem
.ExplicitCall(
Api.MethodPointer("Test.Foo", "Foo", "main"),
Api.MethodPointer("Test.Main", "Main", "main"),
None,
Vector()
)
@ -2272,15 +2268,15 @@ class RuntimeServerTest
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
"Test.Foo",
"Test.Main",
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Foo",
"Test.Main",
"main",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"Foo",
"Main",
"Any",
None
)
@ -2296,7 +2292,7 @@ class RuntimeServerTest
context.send(
Api.Request(
Api.EditFileNotification(
fooFile,
mainFile,
Seq(
TextEdit(
model.Range(model.Position(2, 25), model.Position(2, 29)),
@ -2317,7 +2313,7 @@ class RuntimeServerTest
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Foo",
"Test.Main",
"lucky",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"Number",
@ -2333,13 +2329,14 @@ class RuntimeServerTest
context.consumeOut shouldEqual List("I'm a modified!")
// Close the file
context.send(Api.Request(Api.CloseFileNotification(fooFile)))
context.send(Api.Request(Api.CloseFileNotification(mainFile)))
context.receiveNone shouldEqual None
context.consumeOut shouldEqual List()
}
it should "recompute expressions without invalidation" in {
context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -2349,6 +2346,12 @@ class RuntimeServerTest
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("Test.Main", "Main", "main"),
@ -2377,7 +2380,8 @@ class RuntimeServerTest
}
it should "recompute expressions invalidating all" in {
context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -2387,6 +2391,12 @@ class RuntimeServerTest
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("Test.Main", "Main", "main"),
@ -2421,7 +2431,8 @@ class RuntimeServerTest
}
it should "recompute expressions invalidating some" in {
context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -2431,6 +2442,12 @@ class RuntimeServerTest
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("Test.Main", "Main", "main"),
@ -2467,7 +2484,8 @@ class RuntimeServerTest
}
it should "return error when module not found" in {
context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(context.Main.code)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -2477,6 +2495,12 @@ class RuntimeServerTest
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
context.send(
Api.Request(
@ -2500,7 +2524,8 @@ class RuntimeServerTest
}
it should "return error when constructor not found" in {
context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -2510,6 +2535,12 @@ class RuntimeServerTest
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
context.send(
Api.Request(
@ -2536,7 +2567,8 @@ class RuntimeServerTest
}
it should "return error when method not found" in {
context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -2546,6 +2578,12 @@ class RuntimeServerTest
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
context.send(
Api.Request(
@ -2582,7 +2620,7 @@ class RuntimeServerTest
|bar x y = x + y
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
context.writeMain(contents)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
@ -2590,6 +2628,12 @@ class RuntimeServerTest
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
context.send(
Api.Request(
@ -2623,7 +2667,7 @@ class RuntimeServerTest
|bar x y = x + y
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
context.writeMain(contents)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
@ -2631,6 +2675,12 @@ class RuntimeServerTest
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
context.send(
Api.Request(
@ -2662,7 +2712,7 @@ class RuntimeServerTest
|bar x y = x + y
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
context.writeMain(contents)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
@ -2670,6 +2720,12 @@ class RuntimeServerTest
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
context.send(
Api.Request(
@ -2707,7 +2763,7 @@ class RuntimeServerTest
|main = Number.pi
|""".stripMargin
val contents = metadata.appendToCode(code)
context.writeMain(contents)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
@ -2715,6 +2771,12 @@ class RuntimeServerTest
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
context.send(
Api.Request(
@ -2741,7 +2803,8 @@ class RuntimeServerTest
}
it should "skip side effects when evaluating cached expression" in {
context.writeMain(context.Main2.code)
val contents = context.Main2.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -2751,6 +2814,12 @@ class RuntimeServerTest
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("Test.Main", "Main", "main"),
@ -2780,7 +2849,8 @@ class RuntimeServerTest
}
it should "emit visualisation update when expression is evaluated" in {
context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(context.Main.code)
val visualisationFile =
context.writeInSrcDir("Visualisation", context.Visualisation.code)
@ -2804,6 +2874,12 @@ class RuntimeServerTest
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("Test.Main", "Main", "main"),
@ -2813,11 +2889,51 @@ class RuntimeServerTest
context.send(
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
)
context.receive(5) should contain theSameElementsAs Seq(
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),
Api.Response(
Api.SuggestionsDatabaseIndexUpdateNotification(
Seq(
Api.IndexedModule(
visualisationFile,
context.Visualisation.code,
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"encode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"incAndEncode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
)
)
)
)
)
),
context.executionSuccessful(contextId)
)
@ -2922,6 +3038,7 @@ class RuntimeServerTest
)
)
)
context.receiveNone shouldEqual None
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, false))
)
@ -2943,11 +3060,51 @@ class RuntimeServerTest
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
)
context.receive(6) should contain theSameElementsAs Seq(
context.receive(7) 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),
Api.Response(
Api.SuggestionsDatabaseIndexUpdateNotification(
Seq(
Api.IndexedModule(
visualisationFile,
context.Visualisation.code,
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"encode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"incAndEncode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
)
)
)
)
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
@ -3105,16 +3262,22 @@ class RuntimeServerTest
}
it should "be able to modify visualisations" in {
context.writeMain(context.Main.code)
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,
false
true
)
)
)
@ -3223,16 +3386,22 @@ class RuntimeServerTest
}
it should "not emit visualisation updates when visualisation is detached" in {
context.writeMain(context.Main.code)
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,
false
true
)
)
)
@ -3345,7 +3514,8 @@ class RuntimeServerTest
}
it should "rename a project" in {
context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -3355,6 +3525,12 @@ class RuntimeServerTest
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// open file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, true))
)
context.receiveNone shouldEqual None
// push main
context.send(
Api.Request(

View File

@ -0,0 +1,192 @@
package org.enso.interpreter.test.instrument
import java.io.{ByteArrayOutputStream, File}
import java.nio.ByteBuffer
import java.nio.file.{Files, Paths}
import java.util.UUID
import java.util.concurrent.{LinkedBlockingQueue, TimeUnit}
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.testkit.OsSpec
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 StdlibRuntimeServerTest
extends AnyFlatSpec
with Matchers
with BeforeAndAfterEach
with OsSpec {
final val ContextPathSeparator: String =
if (isWindows) ";" else ":"
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 stdlib: File =
Paths.get("../../distribution/std-lib/Base").toFile.getAbsoluteFile
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,
toPackagesPath(pkg.root.getAbsolutePath, stdlib.toString)
)
.option(RuntimeOptions.LOG_LEVEL, "WARNING")
.option(RuntimeOptions.INTERPRETER_SEQUENTIAL_COMMAND_EXECUTION, "true")
.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)
def toPackagesPath(paths: String*): String =
paths.mkString(ContextPathSeparator)
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(3, TimeUnit.SECONDS))
}
def receive(timeout: Long): Option[Api.Response] = {
Option(messageQueue.poll(timeout, TimeUnit.SECONDS))
}
def receiveN(n: Int): List[Api.Response] = {
Iterator.continually(receive).take(n).flatten.toList
}
def receiveN(n: Int, timeout: Long): List[Api.Response] = {
Iterator.continually(receive(timeout)).take(n).flatten.toList
}
def consumeOut: List[String] = {
val result = out.toString
out.reset()
result.linesIterator.toList
}
def executionSuccessful(contextId: UUID): Api.Response =
Api.Response(Api.ExecutionSuccessful(contextId))
}
override protected def beforeEach(): Unit = {
context = new TestContext("Test")
val Some(Api.Response(_, Api.InitializedNotification())) = context.receive
}
it should "import Base modules" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val code =
"""from Base import all
|
|main = IO.println "Hello World!"
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// open file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, true))
)
context.receiveNone shouldEqual None
// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
val response = context.receiveN(3, timeout = 30)
response.length shouldEqual 3
response should contain allOf (
Api.Response(requestId, Api.PushContextResponse(contextId)),
context.executionSuccessful(contextId)
)
response.collect {
case Api.Response(
None,
Api.SuggestionsDatabaseIndexUpdateNotification(xs)
) =>
xs.nonEmpty shouldBe true
xs.flatMap(
_.updates.headOption.map(_.suggestion.module)
) should not contain "Test.Main"
} should have length 1
context.consumeOut shouldEqual List("Hello World!")
}
}

View File

@ -2,7 +2,8 @@ searcher {
db {
url = "jdbc:sqlite::memory:"
driver = "org.sqlite.JDBC"
connectionPool = "HikariCP"
connectionPool = disabled
properties.journal_mode = "wal"
numThreads = 1
}
}

View File

@ -20,6 +20,14 @@ trait FileVersionsRepo[F[_]] {
*/
def setVersion(file: File, digest: Array[Byte]): F[Option[Array[Byte]]]
/** Update the version if it differs from the recorded version.
*
* @param file the file path
* @param digest the version digest
* @return `true` if the version has been updated
*/
def updateVersion(file: File, digest: Array[Byte]): F[Boolean]
/** Remove the version record.
*
* @param file the file path

View File

@ -73,6 +73,13 @@ trait SuggestionsRepo[F[_]] {
*/
def removeByModule(name: String): F[(Long, Seq[Long])]
/** Remove all suggestions by module names.
*
* @param modules the list of modules to remove
* @return the current database version and a list of removed suggestion ids
*/
def removeAllByModule(modules: Seq[String]): F[(Long, Seq[Long])]
/** Remove a list of suggestions.
*
* @param suggestions the suggestions to remove

View File

@ -77,6 +77,12 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
override def removeByModule(name: String): Future[(Long, Seq[Long])] =
db.run(removeByModuleQuery(name))
/** @inheritdoc */
override def removeAllByModule(
modules: Seq[String]
): Future[(Long, Seq[Long])] =
db.run(removeAllByModuleQuery(modules))
/** @inheritdoc */
override def removeAll(
suggestions: Seq[Suggestion]
@ -137,7 +143,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
suggestions <- joined.result.map(joinedToSuggestionEntries)
version <- currentVersionQuery
} yield (version, suggestions)
query.transactionally
query
}
/** The query to get the suggestions by the method call info.
@ -204,7 +210,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
results <- searchAction
version <- currentVersionQuery
} yield (version, results)
query.transactionally
query
}
/** The query to select the suggestion by id.
@ -234,7 +240,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
}
_ <- incrementVersionQuery
} yield id
query.transactionally.asTry.map {
query.asTry.map {
case Failure(_) => None
case Success(id) => Some(id)
}
@ -252,7 +258,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
ids <- DBIO.sequence(suggestions.map(insertQuery))
version <- currentVersionQuery
} yield (version, ids)
query.transactionally
query
}
/** The query to remove the suggestion.
@ -274,7 +280,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
n <- selectQuery.delete
_ <- if (n > 0) incrementVersionQuery else DBIO.successful(())
} yield rows.flatMap(_.id).headOption
deleteQuery.transactionally
deleteQuery
}
/** The query to remove the suggestions by module name
@ -289,7 +295,24 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
n <- selectQuery.delete
version <- if (n > 0) incrementVersionQuery else currentVersionQuery
} yield version -> rows.flatMap(_.id)
deleteQuery.transactionally
deleteQuery
}
/** The query to remove all suggestions by module names.
*
* @param modules the list of modules to remove
* @return the current database version and a list of removed suggestion ids
*/
private def removeAllByModuleQuery(
modules: Seq[String]
): DBIO[(Long, Seq[Long])] = {
val selectQuery = Suggestions.filter(_.module inSet modules)
val deleteQuery = for {
rows <- selectQuery.result
n <- selectQuery.delete
version <- if (n > 0) incrementVersionQuery else currentVersionQuery
} yield version -> rows.flatMap(_.id)
deleteQuery
}
/** The query to remove a list of suggestions.
@ -304,7 +327,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
ids <- DBIO.sequence(suggestions.map(removeQuery))
version <- currentVersionQuery
} yield (version, ids)
query.transactionally
query
}
/** The query to update a suggestion.
@ -342,7 +365,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
if (ids.exists(_.nonEmpty)) incrementVersionQuery
else currentVersionQuery
} yield (version, ids)
query.transactionally
query
}
/** The query to update the project name.

View File

@ -1,6 +1,7 @@
package org.enso.searcher.sql
import java.io.File
import java.util
import org.enso.searcher.FileVersionsRepo
import slick.jdbc.SQLiteProfile.api._
@ -25,6 +26,10 @@ final class SqlVersionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
): Future[Option[Array[Byte]]] =
db.run(setVersionQuery(file, digest))
/** @inheritdoc */
override def updateVersion(file: File, digest: Array[Byte]): Future[Boolean] =
db.run(updateVersionQuery(file, digest))
/** @inheritdoc */
override def remove(file: File): Future[Unit] =
db.run(removeQuery(file))
@ -79,6 +84,24 @@ final class SqlVersionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
query.transactionally
}
/** The query to update the version if it differs from the recorded version.
*
* @param file the file path
* @param version the version digest
* @return `true` if the version has been updated
*/
private def updateVersionQuery(
file: File,
version: Array[Byte]
): DBIO[Boolean] =
for {
repoVersion <- getVersionQuery(file)
versionsEquals = repoVersion.fold(false)(compareVersions(_, version))
_ <-
if (!versionsEquals) setVersionQuery(file, version)
else DBIO.successful(None)
} yield !versionsEquals
/** The query to remove the version record.
*
* @param file the file path
@ -91,6 +114,9 @@ final class SqlVersionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
query.delete >> DBIO.successful(())
}
private def compareVersions(v1: Array[Byte], v2: Array[Byte]): Boolean =
util.Arrays.equals(v1, v2)
}
object SqlVersionsRepo {

View File

@ -1 +0,0 @@
searcher.db.numThreads = 1

View File

@ -63,7 +63,7 @@ class FileVersionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
util.Arrays.equals(v2.get, digest) shouldBe true
}
"update digest" taggedAs Retry in withRepo { repo =>
"set digest" taggedAs Retry in withRepo { repo =>
val file = new File("/foo/bar")
val digest1 = nextDigest()
val digest2 = nextDigest()
@ -82,6 +82,32 @@ class FileVersionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
util.Arrays.equals(v3.get, digest2) shouldBe true
}
"update digest" taggedAs Retry in withRepo { repo =>
val file = new File("/foo/bazz")
val digest1 = nextDigest()
val digest2 = nextDigest()
val digest3 = nextDigest()
val action =
for {
b1 <- repo.updateVersion(file, digest1)
v2 <- repo.setVersion(file, digest2)
b2 <- repo.updateVersion(file, digest2)
b3 <- repo.updateVersion(file, digest3)
b4 <- repo.updateVersion(file, digest3)
v3 <- repo.getVersion(file)
} yield (v2, v3, b1, b2, b3, b4)
val (v2, v3, b1, b2, b3, b4) = Await.result(action, Timeout)
v2 shouldBe a[Some[_]]
v3 shouldBe a[Some[_]]
util.Arrays.equals(v2.get, digest1) shouldBe true
util.Arrays.equals(v3.get, digest3) shouldBe true
b1 shouldBe true
b2 shouldBe false
b3 shouldBe true
b4 shouldBe false
}
"delete digest" taggedAs Retry in withRepo { repo =>
val file = new File("/foo/bar")
val digest = nextDigest()

View File

@ -47,10 +47,14 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
"get all suggestions" taggedAs Retry in withRepo { repo =>
val action =
for {
_ <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.local)
_ <- repo.insertAll(
Seq(
suggestion.atom,
suggestion.method,
suggestion.function,
suggestion.local
)
)
all <- repo.getAll
} yield all._2
@ -168,12 +172,33 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
"remove suggestions by module name" taggedAs Retry in withRepo { repo =>
val action = for {
id1 <- repo.insert(suggestion.atom)
id2 <- repo.insert(suggestion.method)
id3 <- repo.insert(suggestion.function)
id4 <- repo.insert(suggestion.local)
(_, ids) <- repo.removeByModule(suggestion.atom.module)
} yield (Seq(id1, id2, id3, id4).flatten, ids)
(_, idsIns) <- repo.insertAll(
Seq(
suggestion.atom,
suggestion.method,
suggestion.function,
suggestion.local
)
)
(_, idsRem) <- repo.removeByModule(suggestion.atom.module)
} yield (idsIns.flatten, idsRem)
val (inserted, removed) = Await.result(action, Timeout)
inserted should contain theSameElementsAs removed
}
"remove all suggestions by module name" taggedAs Retry in withRepo { repo =>
val action = for {
(_, idsIns) <- repo.insertAll(
Seq(
suggestion.atom,
suggestion.method,
suggestion.function,
suggestion.local
)
)
(_, idsRem) <- repo.removeAllByModule(Seq(suggestion.atom.module))
} yield (idsIns.flatten, idsRem)
val (inserted, removed) = Await.result(action, Timeout)
inserted should contain theSameElementsAs removed
@ -181,10 +206,14 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
"remove all suggestions" taggedAs Retry in withRepo { repo =>
val action = for {
id1 <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
id4 <- repo.insert(suggestion.local)
(_, Seq(id1, _, _, id4)) <- repo.insertAll(
Seq(
suggestion.atom,
suggestion.method,
suggestion.function,
suggestion.local
)
)
(_, ids) <- repo.removeAll(Seq(suggestion.atom, suggestion.local))
} yield (Seq(id1, id4), ids)
@ -287,6 +316,37 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
v3 shouldEqual v4
}
"change version after remove all by module name" taggedAs Retry in withRepo {
repo =>
val action = for {
v1 <- repo.currentVersion
_ <- repo.insert(suggestion.local)
v2 <- repo.currentVersion
(v3, _) <- repo.removeAllByModule(Seq(suggestion.local.module))
} yield (v1, v2, v3)
val (v1, v2, v3) = Await.result(action, Timeout)
v1 should not equal v2
v2 should not equal v3
}
"not change version after failed remove all by module name" taggedAs Retry in withRepo {
repo =>
val action = for {
v1 <- repo.currentVersion
_ <- repo.insert(suggestion.local)
v2 <- repo.currentVersion
_ <- repo.removeAllByModule(Seq(suggestion.local.module))
v3 <- repo.currentVersion
(v4, _) <- repo.removeAllByModule(Seq(suggestion.local.module))
} yield (v1, v2, v3, v4)
val (v1, v2, v3, v4) = Await.result(action, Timeout)
v1 should not equal v2
v2 should not equal v3
v3 shouldEqual v4
}
"change version after remove all suggestions" taggedAs Retry in withRepo {
repo =>
val action = for {

View File

@ -14,12 +14,20 @@ trait OsSpec extends TestSuite {
override def withFixture(test: NoArgTest): Outcome = {
if (test.tags.contains(OsUnix.name)) {
if (SystemUtils.IS_OS_UNIX) super.withFixture(test) else Pending
if (isUnix) super.withFixture(test) else Pending
} else if (test.tags.contains(OsWindows.name)) {
if (SystemUtils.IS_OS_WINDOWS) super.withFixture(test) else Pending
if (isWindows) super.withFixture(test) else Pending
} else {
super.withFixture(test)
}
}
/** @return `true` if this is a Unix-like system. */
protected def isUnix: Boolean =
SystemUtils.IS_OS_UNIX
/** @return `true` if this is a Windows system. */
protected def isWindows: Boolean =
SystemUtils.IS_OS_WINDOWS
}