From 87ce78615afecb8bd8d586c798c97ab5e28083cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Wa=C5=9Bko?= Date: Fri, 1 Sep 2023 22:20:04 +0200 Subject: [PATCH] Change layout of local library search path in order to be able to move `Round_Spec.enso` back to `Tests` (#7634) - Closes #7633 - Moves `Round_Spec.enso` from published `Standard.Test` into our `test/Tests` project; the `Table_Tests` that depend on it, simply `import enso_dev.Tests`. - Changes the layout of the local libraries directory: - It used to be `root//`. - Now it is `root/` - the namespace and name are now read from `package.yaml` instead. - Adds the parent directory of the current project to the default `ENSO_LIBRARY_PATH`. - It is treated as a secondary path, so the default `ENSO_HOME/lib` still takes precedence. - This allows projects to reference and load 'sibling' projects easily - the only requirement is for the project to enable `prefer-local-libraries: true` or add the other local project to its edition. The edition resolution logic is **not changed**. --- CHANGELOG.md | 3 + docs/libraries/editions.md | 54 +++++--- docs/libraries/sharing.md | 12 +- .../enso/languageserver/boot/MainModule.scala | 12 +- .../libraries/EditionReferenceResolver.scala | 2 +- .../libraries/LocalLibraryManager.scala | 36 ++--- .../handler/LibraryPreinstallHandler.scala | 9 +- .../libraries/LocalLibraryManagerSpec.scala | 74 +++++++++++ .../websocket/json/BaseServerTest.scala | 12 +- .../websocket/json/LibrariesTest.scala | 63 +++++---- .../enso/runner/DependencyPreinstaller.scala | 3 +- .../runtime/DefaultPackageRepository.scala | 8 +- .../distribution/DistributionManager.scala | 4 +- .../DefaultLibraryProvider.scala | 12 +- .../librarymanager/LibraryLocations.scala | 11 +- .../enso/librarymanager/LibraryResolver.scala | 1 - .../local/DefaultLocalLibraryProvider.scala | 125 ++++++++++++++++-- .../local/LocalLibraryProvider.scala | 13 +- .../library_in_both_dirs/package.yaml | 3 + .../new_folder1/package.yaml | 3 + .../Simple_Library_1/package.yaml | 3 + .../Simple_Library_2/package.yaml | 3 + .../test-library-path/ambiguous1/package.yaml | 3 + .../test-library-path/ambiguous2/package.yaml | 3 + .../library_in_both_dirs/package.yaml | 3 + .../librarymanager/LibraryResolverSpec.scala | 4 + .../local/LocalLibraryProviderSpec.scala | 58 ++++++++ .../src/main/scala/org/enso/pkg/Config.scala | 4 +- .../enso/testkit/WithTemporaryDirectory.scala | 10 +- test/Examples_Tests/package.yaml | 2 +- test/Table_Tests/package.yaml | 3 +- .../Column_Operations_Spec.enso | 3 +- test/Tests/src/Data/Numbers_Spec.enso | 3 +- .../Tests/src/Data}/Round_Spec.enso | 0 34 files changed, 428 insertions(+), 134 deletions(-) create mode 100644 engine/language-server/src/test/scala/org/enso/languageserver/libraries/LocalLibraryManagerSpec.scala create mode 100644 lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/secondary-test-library-path/library_in_both_dirs/package.yaml create mode 100644 lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/secondary-test-library-path/new_folder1/package.yaml create mode 100644 lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/Simple_Library_1/package.yaml create mode 100644 lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/Simple_Library_2/package.yaml create mode 100644 lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/ambiguous1/package.yaml create mode 100644 lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/ambiguous2/package.yaml create mode 100644 lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/library_in_both_dirs/package.yaml create mode 100644 lib/scala/library-manager/src/test/scala/org/enso/librarymanager/local/LocalLibraryProviderSpec.scala rename {distribution/lib/Standard/Test/0.0.0-dev/src/Shared => test/Tests/src/Data}/Round_Spec.enso (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64fa45855755..70f5392a4f22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -936,6 +936,8 @@ - [Allow Java Enums in case of branches][7607] - [Notification about the project rename action][7613] - [Use `numpy` & co. from Enso!][7678] +- [Changed layout of local libraries directory, making it easier to reference + projects next to each other][7634] [3227]: https://github.com/enso-org/enso/pull/3227 [3248]: https://github.com/enso-org/enso/pull/3248 @@ -1071,6 +1073,7 @@ [7607]: https://github.com/enso-org/enso/pull/7607 [7613]: https://github.com/enso-org/enso/pull/7613 [7678]: https://github.com/enso-org/enso/pull/7678 +[7634]: https://github.com/enso-org/enso/pull/7634 # Enso 2.0.0-alpha.18 (2021-10-12) diff --git a/docs/libraries/editions.md b/docs/libraries/editions.md index 038c9522d07c..9b50afd388fc 100644 --- a/docs/libraries/editions.md +++ b/docs/libraries/editions.md @@ -189,36 +189,48 @@ so edition files that already exist on disk are not redownloaded. Below are listed the steps that are taken when resolving an import of library `Foo.Bar`: -1. If and only if the project has `prefer-local-libraries` set to `true` and if - any directory on the library path contains `Foo/Bar`, that local instance is - chosen as the library that should be used, regardless of the version that is - there; +1. If and only if the project has `prefer-local-libraries` set to `true`, the + library path is searched for sub-directories containing Enso packages. If any + of such packages has a `package.yaml` that defines `namespace:Foo` and + `name: Bar`, that local instance of the library is chosen. In this particular + scenario the version check is skipped - whatever version is present in the + local library path is used. 2. Otherwise, the list of libraries defined directly in the `edition` section of `package.yaml` of the current project is checked, and if the library is defined there, it is selected. 3. Otherwise, any parent editions are consulted; if they too do not contain the library that we are searching for, an error is reported. 4. Once we know the library version to be used: - 1. If the repository associated with the library is `local`, the library path - is searched for the first directory to contain `Foo/Bar` and this path is - loaded. If the library is not present on the library path, an error is - reported. + 1. If the repository associated with the library is `local`, the local + library path is searched for the first directory to contain the requested + library and this path is loaded. If the library is not present on the + library path, an error is reported. 2. Otherwise, the edition must have defined an exact `` of the library that is supposed to be used. - 3. If the library is already downloaded in the local repository cache, that - is the directory `$ENSO_DATA_DIRECTORY/lib/Foo/Bar/` exists, that + 3. If the library is already downloaded in the local repository cache (the + directory `$ENSO_DATA_DIRECTORY/lib/Foo/Bar/` exists), that package is loaded. 4. Otherwise, the library is missing and must be downloaded from its associated repository (and placed in the cache as above). -By default, the library path is `/libraries/` but it can be -overridden by setting the `ENSO_LIBRARY_PATH` environment variable. It may -include a list of directories (separated by the system specific path separator); -the first directory on the list has the highest precedence. - -In particular, if `prefer-local-libraries` is `false`, and the edition does not -define a library at all, when trying to resolve such a library, it is reported -as not found even if a local version of it exists. That is because -auto-discovery of local libraries is only done with `prefer-local-libraries` set -to `true`. In all other cases, the `local` repository overrides should be set -explicitly. +By default, the local library path consists of two directories: + +- `/libraries/`, +- the parent directory of the currently opened project. + +This allows the user to access libraries that are placed next to the current +project (although ones located in the Enso home still take precedence). Still, +to access local libraries they either have to be defined in the edition, or the +`prefer-local-libraries` flag must be set to `true`. + +The local library search path can be overridden by setting the +`ENSO_LIBRARY_PATH` environment variable. It may include a list of directories +(separated by the system specific path separator); the first directory on the +list has the highest precedence. If the environment variable is defined, it +overrides the default paths. + +If `prefer-local-libraries` is `false`, and the edition does not define a +library at all, when trying to resolve such a library, it is reported as not +found even if a local version of it exists. That is because auto-discovery of +local libraries is only done with `prefer-local-libraries` set to `true`. In all +other cases, the `local` repository overrides should be set explicitly. diff --git a/docs/libraries/sharing.md b/docs/libraries/sharing.md index c2e64cf50401..0d0c287cd025 100644 --- a/docs/libraries/sharing.md +++ b/docs/libraries/sharing.md @@ -23,19 +23,11 @@ To prepare the project for sharing, make sure that it has a proper `namespace` field set in `package.yaml`. It should be set to something unique, like your username. -> **NOTE**: The field `namespace` is a temporary workaround and in the near -> future it will be deprecated and handled mostly automatically. For now however -> you need to set it properly. - To share an Enso library, all you need to do is to package the project into an archive (for example ZIP) and share it (through e-mail, cloud drive services etc.) with your peers. Now to be able to use the library that was shared with -you, you need to extract it to the directory -`~/enso/libraries//` (where on Windows `~` should be -interpreted as your user home directory). To make sure that the library is -extracted correctly, make sure that under the path -`~/enso/libraries///package.yaml` and that its -`namespace` field has the same value as the name of the `` directory. +you, you need to extract it to the directory `~/enso/libraries/` +(where on Windows `~` should be interpreted as your user home directory). Now you need to set up your project properly to be able to use this unpublished library. The simplest way to do that is to set `prefer-local-libraries` in your diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala index 4e0820451f2a..f8d9b698473b 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala @@ -356,14 +356,18 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) { "project-settings-manager" ) + val libraryLocations = + LibraryLocations.resolve( + distributionManager, + Some(languageHome), + Some(contentRoot.file.toPath) + ) + val localLibraryManager = system.actorOf( - LocalLibraryManager.props(contentRoot.file, distributionManager), + LocalLibraryManager.props(contentRoot.file, libraryLocations), "local-library-manager" ) - val libraryLocations = - LibraryLocations.resolve(distributionManager, Some(languageHome)) - val libraryConfig = LibraryConfig( localLibraryManager = localLibraryManager, editionReferenceResolver = editionReferenceResolver, diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReferenceResolver.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReferenceResolver.scala index 7edef3432c9e..8c866e6cf18d 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReferenceResolver.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReferenceResolver.scala @@ -10,7 +10,7 @@ import scala.util.Try /** Resolves [[EditionReference]] to a raw or resolved edition. */ class EditionReferenceResolver( - projectRoot: File, + val projectRoot: File, editionProvider: EditionProvider, editionResolver: EditionResolver ) { diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala index 0c7add32c20a..aabfa4532830 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala @@ -2,13 +2,11 @@ package org.enso.languageserver.libraries import akka.actor.Props import com.typesafe.scalalogging.LazyLogging -import org.enso.distribution.{DistributionManager, FileSystem} +import org.enso.distribution.FileSystem import org.enso.editions.{Editions, LibraryName} import org.enso.languageserver.libraries.LocalLibraryManagerProtocol._ -import org.enso.librarymanager.local.{ - DefaultLocalLibraryProvider, - LocalLibraryProvider -} +import org.enso.librarymanager.LibraryLocations +import org.enso.librarymanager.local.DefaultLocalLibraryProvider import org.enso.librarymanager.published.repository.LibraryManifest import org.enso.pkg.validation.NameValidation import org.enso.pkg.{Config, Contact, Package, PackageManager} @@ -16,17 +14,16 @@ import org.enso.yaml.YamlHelper import java.io.File import java.nio.file.{Files, Path} - import scala.util.{Success, Try} /** An Actor that manages local libraries. */ class LocalLibraryManager( currentProjectRoot: File, - distributionManager: DistributionManager + libraryLocations: LibraryLocations ) extends BlockingSynchronizedRequestHandler with LazyLogging { val localLibraryProvider = new DefaultLocalLibraryProvider( - distributionManager.paths.localLibrariesSearchPaths.toList + libraryLocations.localLibrarySearchPaths ) override def requestStage: Receive = { case request: Request => @@ -77,7 +74,7 @@ class LocalLibraryManager( // TODO [RW] make the exceptions more relevant val possibleRoots = LazyList - .from(distributionManager.paths.localLibrariesSearchPaths) + .from(libraryLocations.localLibrarySearchPaths) .filter { path => Try { if (Files.notExists(path)) Files.createDirectories(path) } Files.isWritable(path) @@ -88,9 +85,10 @@ class LocalLibraryManager( ) } - val libraryPath = - LocalLibraryProvider.resolveLibraryPath(librariesRoot, libraryName) + val libraryPath = librariesRoot.resolve(libraryName.name) if (Files.exists(libraryPath)) { + // TODO [RW] we could try finding alternative names (as directory name does not matter for local libraries), to find a free name + // This can be done as part of #1877 throw new RuntimeException("Local library already exists") } @@ -121,17 +119,7 @@ class LocalLibraryManager( } yield ListLocalLibrariesResponse(libraryEntries) private def findLocalLibraries(): Try[Seq[LibraryName]] = Try { - for { - searchPathRoot <- distributionManager.paths.localLibrariesSearchPaths - namespaceDir <- FileSystem - .listDirectory(searchPathRoot) - .filter(Files.isDirectory(_)) - nameDir <- FileSystem - .listDirectory(namespaceDir) - .filter(Files.isDirectory(_)) - namespace = namespaceDir.getFileName.toString - name = nameDir.getFileName.toString - } yield LibraryName(namespace, name) + localLibraryProvider.findAvailableLocalLibraries() } /** Finds the path on the filesystem to a local library. */ @@ -252,8 +240,8 @@ class LocalLibraryManager( object LocalLibraryManager { def props( currentProjectRoot: File, - distributionManager: DistributionManager + libraryLocations: LibraryLocations ): Props = Props( - new LocalLibraryManager(currentProjectRoot, distributionManager) + new LocalLibraryManager(currentProjectRoot, libraryLocations) ) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPreinstallHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPreinstallHandler.scala index b1d801d7397f..95d23ecd6e8c 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPreinstallHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPreinstallHandler.scala @@ -168,15 +168,15 @@ class LibraryPreinstallHandler( case Left(error) => val errorMessage = error match { case InternalError(throwable) => - FileSystemError(s"Internal error: ${throwable.getMessage}") + FileSystemError(s"Internal error: ${throwable.toString}") case DependencyGatheringError(throwable) => - DependencyDiscoveryError(throwable.getMessage) + DependencyDiscoveryError(throwable.toString) case InstallerError(Error.NotResolved(_)) => LibraryNotResolved(libraryName) case InstallerError(Error.RequestedLocalLibraryDoesNotExist) => LocalLibraryNotFound(libraryName) case InstallerError(Error.DownloadFailed(version, reason)) => - LibraryDownloadError(libraryName, version, reason.getMessage) + LibraryDownloadError(libraryName, version, reason.toString) } replyTo ! ResponseError( Some(requestId), @@ -216,7 +216,8 @@ class LibraryPreinstallHandler( progressReporter = notificationReporter, languageHome = config.installerConfig.languageHome, edition = edition, - preferLocalLibraries = preferLocalLibraries + preferLocalLibraries = preferLocalLibraries, + projectRoot = Some(editionReferenceResolver.projectRoot.toPath) ) dependencyResolver = new DependencyResolver( localLibraryProvider = config.localLibraryProvider, diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/libraries/LocalLibraryManagerSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/LocalLibraryManagerSpec.scala new file mode 100644 index 000000000000..651b1f6f1994 --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/LocalLibraryManagerSpec.scala @@ -0,0 +1,74 @@ +package org.enso.languageserver.libraries + +import akka.actor.ActorSystem +import akka.testkit._ +import org.enso.distribution.FileSystem.PathSyntax +import org.enso.editions.LibraryName +import org.enso.librarymanager.LibraryLocations +import org.enso.pkg.PackageManager +import org.enso.testkit.WithTemporaryDirectory +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpecLike +import org.scalatest.BeforeAndAfterAll +import org.scalatest.time.SpanSugar.convertIntToGrainOfTime + +import scala.concurrent.duration.FiniteDuration + +class LocalLibraryManagerSpec + extends TestKit(ActorSystem("TestSystem")) + with ImplicitSender + with AnyWordSpecLike + with Matchers + with BeforeAndAfterAll + with WithTemporaryDirectory { + + val Timeout: FiniteDuration = 10.seconds + + override def afterAll(): Unit = { + TestKit.shutdownActorSystem(system) + } + + "LocalLibraryManager" should { + "find the libraries it has itself created" in { + val projectRoot = getTestDirectory / "project-root" + PackageManager.Default.create(projectRoot.toFile, "Test_Project_123") + val localLibraryRoot = getTestDirectory / "local-library-root" + val libraryLocations = LibraryLocations( + List(localLibraryRoot), + getTestDirectory / "library-cache-root", + List() + ) + val manager = + system.actorOf( + LocalLibraryManager.props(projectRoot.toFile, libraryLocations) + ) + + val myLibraryName = LibraryName("user456", "My_Library") + + manager ! LocalLibraryManagerProtocol.Create( + myLibraryName, + Seq(), + Seq(), + "CC0" + ) + expectMsg(Timeout, LocalLibraryManagerProtocol.EmptyResponse()) + + manager ! LocalLibraryManagerProtocol.FindLibrary(myLibraryName) + expectMsgPF(Timeout, "FindLibraryResponse") { + case LocalLibraryManagerProtocol.FindLibraryResponse(Some(root)) => + assert(root.location.startsWith(localLibraryRoot)) + } + + manager ! LocalLibraryManagerProtocol.ListLocalLibraries + val foundLibraries = expectMsgPF(Timeout, "ListLocalLibrariesResponse") { + case LocalLibraryManagerProtocol.ListLocalLibrariesResponse( + libraries + ) => + libraries + } + foundLibraries.map(entry => + LibraryName(entry.namespace, entry.name) + ) should contain theSameElementsAs Seq(myLibraryName) + } + } +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala index 99e5208d718c..f48e00b2d7f5 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala @@ -297,16 +297,20 @@ class BaseServerTest ) ) + val libraryLocations = + LibraryLocations.resolve( + distributionManager, + Some(languageHome), + Some(config.projectContentRoot.file.toPath) + ) + val localLibraryManager = system.actorOf( LocalLibraryManager.props( config.projectContentRoot.file, - distributionManager + libraryLocations ) ) - val libraryLocations = - LibraryLocations.resolve(distributionManager, Some(languageHome)) - val libraryConfig = LibraryConfig( localLibraryManager = localLibraryManager, editionReferenceResolver = editionReferenceResolver, diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala index 489f91d19454..936e72512160 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala @@ -53,21 +53,45 @@ class LibrariesTest extends BaseServerTest { "LocalLibraryManager" should { "create a library project and include it on the list of local projects" in { - val client = getInitialisedWsClient() + val client = getInitialisedWsClient() + val testLibraryName = LibraryName("user", "My_Local_Lib") + client.send(json""" { "jsonrpc": "2.0", "method": "library/listLocal", "id": 0 } """) - client.expectJson(json""" - { "jsonrpc": "2.0", - "id": 0, - "result": { - "localLibraries": [] - } + + def getLibraryNameFromJsonDescription(lib: Json): LibraryName = { + inside(lib.asObject) { case Some(obj) => + inside( + ( + obj("namespace").flatMap(_.asString), + obj("name").flatMap(_.asString) + ) + ) { case (Some(namespace), Some(name)) => + LibraryName(namespace, name) } - """) + } + } + + def findLibraryNamesInResponse( + response: Json + ): Option[Vector[LibraryName]] = + for { + obj <- response.asObject + result <- obj("result").flatMap(_.asObject) + libraries <- result("localLibraries").flatMap(_.asArray) + libraryNames = libraries.map(getLibraryNameFromJsonDescription) + } yield libraryNames + + // The resolver may find the current project and other test projects on the path. + val msg1 = client.expectSomeJson() + inside(findLibraryNamesInResponse(msg1)) { case Some(libs) => + // Ensure that before running this test, the library did not exist. + libs should not contain testLibraryName + } client.send(json""" { "jsonrpc": "2.0", @@ -107,23 +131,10 @@ class LibrariesTest extends BaseServerTest { "id": 2 } """) - client.expectJson(json""" - { "jsonrpc": "2.0", - "id": 2, - "result": { - "localLibraries": [ - { - "namespace": "user", - "name": "My_Local_Lib", - "version": { - "type": "LocalLibraryVersion" - }, - "isCached": true - } - ] - } - } - """) + val msg2 = client.expectSomeJson() + inside(findLibraryNamesInResponse(msg2)) { case Some(libs) => + libs should contain(testLibraryName) + } } "fail with LibraryAlreadyExists when creating a library that already " + @@ -275,7 +286,6 @@ class LibrariesTest extends BaseServerTest { val libraryRoot = getTestDirectory .resolve("test_home") .resolve("libraries") - .resolve("user") .resolve("Get_Package_Test_Lib") val packageFile = libraryRoot.resolve(Package.configFileName) val packageConfig = @@ -383,7 +393,6 @@ class LibrariesTest extends BaseServerTest { val libraryRoot = getTestDirectory .resolve("test_home") .resolve("libraries") - .resolve("user") .resolve("Publishable_Lib") val mainSource = libraryRoot.resolve("src").resolve("Main.enso") FileSystem.writeTextFile( diff --git a/engine/runner/src/main/scala/org/enso/runner/DependencyPreinstaller.scala b/engine/runner/src/main/scala/org/enso/runner/DependencyPreinstaller.scala index b357d9726032..14f75393bcd4 100644 --- a/engine/runner/src/main/scala/org/enso/runner/DependencyPreinstaller.scala +++ b/engine/runner/src/main/scala/org/enso/runner/DependencyPreinstaller.scala @@ -79,7 +79,8 @@ object DependencyPreinstaller { ProgressBar.waitWithProgress(task) } }, - Some(languageHome) + languageHome = Some(languageHome), + projectRoot = Some(projectRoot.toPath) ) val dependencyResolver = new DependencyResolver( diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/runtime/DefaultPackageRepository.scala b/engine/runtime/src/main/scala/org/enso/interpreter/runtime/DefaultPackageRepository.scala index 44031d4bdfd0..413a8b9bd57e 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/runtime/DefaultPackageRepository.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/runtime/DefaultPackageRepository.scala @@ -628,6 +628,11 @@ private object DefaultPackageRepository { val editionManager = EditionManager(distributionManager, homeManager) val edition = editionManager.resolveEdition(rawEdition).get + val projectRoot = projectPackage.map { pkg => + val root = pkg.root + Path.of(root.getAbsoluteFile.toUri) + } + val resolvingLibraryProvider = DefaultLibraryProvider.make( distributionManager = distributionManager, @@ -637,7 +642,8 @@ private object DefaultPackageRepository { languageHome = homeManager, edition = edition, preferLocalLibraries = - projectPackage.exists(_.getConfig().preferLocalLibraries) + projectPackage.exists(_.getConfig().preferLocalLibraries), + projectRoot = projectRoot ) new DefaultPackageRepository( resolvingLibraryProvider, diff --git a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/DistributionManager.scala b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/DistributionManager.scala index a67fa7ec49b2..135428c1816b 100644 --- a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/DistributionManager.scala +++ b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/DistributionManager.scala @@ -229,7 +229,9 @@ class DistributionManager(val env: Environment) { env .getEnvPaths(ENSO_LIBRARY_PATH) .getOrElse { - Seq(ensoHome / DistributionManager.Home.LIBRARIES_DIRECTORY) + Seq( + ensoHome / DistributionManager.Home.LIBRARIES_DIRECTORY + ) } /** Name of the file that should be placed in the distribution root to mark it diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/DefaultLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/DefaultLibraryProvider.scala index d59e6ba482d7..264fd5a31f37 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/DefaultLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/DefaultLibraryProvider.scala @@ -21,6 +21,8 @@ import org.enso.librarymanager.published.{ PublishedLibraryProvider } +import java.nio.file.Path + /** A helper class for loading libraries. * * @param localLibraryProvider provider of local (unpublished) libraries @@ -108,6 +110,7 @@ object DefaultLibraryProvider { lockUserInterface: LockUserInterface, progressReporter: ProgressReporter, languageHome: Option[LanguageHome], + projectRoot: Option[Path], edition: Editions.ResolvedEdition, preferLocalLibraries: Boolean ): ResolvingLibraryProvider = { @@ -116,7 +119,8 @@ object DefaultLibraryProvider { resourceManager, lockUserInterface, progressReporter, - languageHome + languageHome, + projectRoot ) new DefaultLibraryProvider( @@ -133,12 +137,14 @@ object DefaultLibraryProvider { resourceManager: ResourceManager, lockUserInterface: LockUserInterface, progressReporter: ProgressReporter, - languageHome: Option[LanguageHome] + languageHome: Option[LanguageHome], + projectRoot: Option[Path] ): ( LocalLibraryProvider, PublishedLibraryProvider with PublishedLibraryCache ) = { - val locations = LibraryLocations.resolve(distributionManager, languageHome) + val locations = + LibraryLocations.resolve(distributionManager, languageHome, projectRoot) val primaryCache = new DownloadingLibraryCache( locations.primaryCacheRoot, TemporaryDirectoryManager(distributionManager, resourceManager), diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryLocations.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryLocations.scala index 6ba341ec1c29..cf413bd52dee 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryLocations.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryLocations.scala @@ -27,13 +27,20 @@ object LibraryLocations { * which provides paths to the distribution and an optional [[LanguageHome]] * which can provide paths to libraries bundled with the current language * version. + * + * If a project root is provided, the local library search path will be + * extended with the parent directory of the current project, allowing to + * search for libraries located next to it. */ def resolve( distributionManager: DistributionManager, - languageHome: Option[LanguageHome] + languageHome: Option[LanguageHome], + projectRoot: Option[Path] ): LibraryLocations = { + val parentDirectorySearchPath = + projectRoot.map(_.toAbsolutePath.getParent.normalize).toList val localLibrarySearchPaths = - distributionManager.paths.localLibrariesSearchPaths.toList + (distributionManager.paths.localLibrariesSearchPaths ++ parentDirectorySearchPath).toList val cacheRoot = distributionManager.paths.cachedLibraries val additionalCacheLocations = { val engineBundleRoot = languageHome.map(_.libraries) diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryResolver.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryResolver.scala index dac04dc1bd51..0ad50cce9dbe 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryResolver.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryResolver.scala @@ -66,7 +66,6 @@ case class LibraryResolver( case Some(parentEdition) => resolveLibraryFromEdition(libraryName, parentEdition) case None => - new Exception("library not found").printStackTrace() Left( LibraryResolutionError( s"The library `$libraryName` is not defined within " + diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala index 78f20f04725e..17b61330cdae 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala @@ -5,10 +5,13 @@ import org.enso.editions.LibraryName import org.enso.librarymanager.LibraryLocations import org.enso.librarymanager.resolved.LibraryRoot import org.enso.logger.masking.MaskedPath +import org.enso.pkg.PackageManager +import java.io.IOException import java.nio.file.{Files, Path} - import scala.annotation.tailrec +import scala.jdk.CollectionConverters.ListHasAsScala +import scala.util.{Failure, Success} /** A default implementation of [[LocalLibraryProvider]]. */ class DefaultLocalLibraryProvider(searchPaths: List[Path]) @@ -32,24 +35,124 @@ class DefaultLocalLibraryProvider(searchPaths: List[Path]) libraryName: LibraryName, searchPaths: List[Path] ): Option[Path] = searchPaths match { - case head :: tail => - val potentialPath = - LocalLibraryProvider.resolveLibraryPath(head, libraryName) - if (Files.exists(potentialPath) && Files.isDirectory(potentialPath)) { - logger.trace( - s"Found a local $libraryName at " + - s"[${MaskedPath(potentialPath).applyMasking()}]." - ) - Some(potentialPath) - } else { + case potentialPath :: tail => + val candidates = findCandidates(libraryName, potentialPath) + if (candidates.isEmpty) { logger.trace( s"Local library $libraryName not found at " + s"[${MaskedPath(potentialPath).applyMasking()}]." ) findLibraryHelper(libraryName, tail) + } else { + if (candidates.size > 1) { + val firstCandidate = candidates.minBy(_.getFileName.toString) + logger.warn( + s"Found multiple libraries with the same name and namespace in a single directory: " + + s"${candidates.map(_.getFileName.toString).mkString(", ")}. " + + s"Choosing the first one (${firstCandidate.getFileName})." + ) + Some(firstCandidate) + } else { + val found = candidates.head + logger.trace( + s"Resolved library [$libraryName] at [${MaskedPath(found).applyMasking()}]." + ) + Some(found) + } } case Nil => None } + + private def findCandidates( + libraryName: LibraryName, + librariesPath: Path + ): List[Path] = try { + if (!Files.isDirectory(librariesPath)) { + warnAboutMissingSearchPath(librariesPath) + return Nil + } + + val subdirectories = Files.list(librariesPath).filter(Files.isDirectory(_)) + subdirectories + .filter { potentialPath => + val isGood = + PackageManager.Default.loadPackage(potentialPath.toFile) match { + case Failure(exception) => + logger.trace( + s"Failed to load the candidate library package description at [${MaskedPath(potentialPath) + .applyMasking()}].", + exception + ) + false + case Success(pkg) => pkg.libraryName == libraryName + } + if (isGood) { + logger.trace( + s"Found candidate library [$libraryName] at [${MaskedPath(potentialPath).applyMasking()}]." + ) + } + isGood + } + .toList + .asScala + .toList + } catch { + case ex @ (_: IOException | _: RuntimeException) => + val maskedPath = MaskedPath(librariesPath).applyMasking() + logger.warn( + s"Exception occurred when scanning library path [$maskedPath]: $ex" + ) + Nil + } + + /** Finds all currently available local libraries. */ + override def findAvailableLocalLibraries(): List[LibraryName] = { + val libraries: List[LibraryName] = searchPaths.flatMap { path => + try { + if (!Files.isDirectory(path)) { + warnAboutMissingSearchPath(path) + Nil + } else { + val subdirectories = + Files.list(path).filter(Files.isDirectory(_)).toList.asScala + subdirectories + .map { potentialPath => + val pkg = PackageManager.Default.loadPackage( + potentialPath.toFile + ) + pkg.map(_.libraryName) + } + .collect { case Success(name) => name } + } + } catch { + case ex @ (_: IOException | _: RuntimeException) => + val maskedPath = MaskedPath(path).applyMasking() + logger.warn( + s"Exception occurred when scanning library path [$maskedPath]: $ex" + ) + Nil + } + } + libraries.distinct + } + + private def warnAboutMissingSearchPath(path: Path): Unit = { + val exists = Files.exists(path) + val suffix = if (exists) "is not a directory" else "does not exist" + val warning = + s"Local library search path [${MaskedPath(path).applyMasking()}] $suffix." + if (alreadyWarned.get(path).contains(suffix)) { + // If we already warned about this path, further warnings get degraded to trace level. + // Only one warning at warning level is emitted. + logger.trace(warning) + } else { + logger.warn(warning) + alreadyWarned.put(path, suffix) + } + } + + private val alreadyWarned = + scala.collection.concurrent.TrieMap.empty[Path, String] } object DefaultLocalLibraryProvider { diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala index 6f7b7433356b..ea3fa4407bc3 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala @@ -1,11 +1,8 @@ package org.enso.librarymanager.local -import org.enso.distribution.FileSystem.PathSyntax import org.enso.editions.LibraryName import org.enso.librarymanager.resolved.LibraryRoot -import java.nio.file.Path - /** A provider for local libraries. */ trait LocalLibraryProvider { @@ -15,13 +12,7 @@ trait LocalLibraryProvider { * @return the location of the requested library, if it is available. */ def findLibrary(libraryName: LibraryName): Option[LibraryRoot] -} -object LocalLibraryProvider { - - /** Resolve a path to the package root of a particular library located in one - * of the local library roots. - */ - def resolveLibraryPath(root: Path, libraryName: LibraryName): Path = - root / libraryName.namespace / libraryName.name + /** Finds all currently available local libraries. */ + def findAvailableLocalLibraries(): List[LibraryName] } diff --git a/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/secondary-test-library-path/library_in_both_dirs/package.yaml b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/secondary-test-library-path/library_in_both_dirs/package.yaml new file mode 100644 index 000000000000..0f3f239e82cb --- /dev/null +++ b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/secondary-test-library-path/library_in_both_dirs/package.yaml @@ -0,0 +1,3 @@ +name: Library_In_Both_Dirs +namespace: user123 +version: 1.0.0 diff --git a/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/secondary-test-library-path/new_folder1/package.yaml b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/secondary-test-library-path/new_folder1/package.yaml new file mode 100644 index 000000000000..db05c19b9bd0 --- /dev/null +++ b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/secondary-test-library-path/new_folder1/package.yaml @@ -0,0 +1,3 @@ +name: Library_1 +namespace: user1 +version: 1.0.0 diff --git a/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/Simple_Library_1/package.yaml b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/Simple_Library_1/package.yaml new file mode 100644 index 000000000000..75ae48915680 --- /dev/null +++ b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/Simple_Library_1/package.yaml @@ -0,0 +1,3 @@ +name: Simple_Library +namespace: dev1 +version: 1.0.0 diff --git a/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/Simple_Library_2/package.yaml b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/Simple_Library_2/package.yaml new file mode 100644 index 000000000000..f301beba4197 --- /dev/null +++ b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/Simple_Library_2/package.yaml @@ -0,0 +1,3 @@ +name: Simple_Library +namespace: dev2 +version: 1.0.0 diff --git a/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/ambiguous1/package.yaml b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/ambiguous1/package.yaml new file mode 100644 index 000000000000..da8918843621 --- /dev/null +++ b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/ambiguous1/package.yaml @@ -0,0 +1,3 @@ +name: Ambiguous_Library +namespace: ambiguous_developer +version: 1.0.0 diff --git a/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/ambiguous2/package.yaml b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/ambiguous2/package.yaml new file mode 100644 index 000000000000..da8918843621 --- /dev/null +++ b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/ambiguous2/package.yaml @@ -0,0 +1,3 @@ +name: Ambiguous_Library +namespace: ambiguous_developer +version: 1.0.0 diff --git a/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/library_in_both_dirs/package.yaml b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/library_in_both_dirs/package.yaml new file mode 100644 index 000000000000..0f3f239e82cb --- /dev/null +++ b/lib/scala/library-manager/src/test/resources/org/enso/librarymanager/local/test-library-path/library_in_both_dirs/package.yaml @@ -0,0 +1,3 @@ +name: Library_In_Both_Dirs +namespace: user123 +version: 1.0.0 diff --git a/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala b/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala index ce5d66e29dff..e854a7673ac0 100644 --- a/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala +++ b/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala @@ -66,6 +66,10 @@ class LibraryResolverSpec fixtures .get(libraryName) .map(LibraryRoot(_)) + + /** @inheritdoc */ + override def findAvailableLocalLibraries(): List[LibraryName] = + fixtures.keys.toList } val localLibraries = Map( diff --git a/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/local/LocalLibraryProviderSpec.scala b/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/local/LocalLibraryProviderSpec.scala new file mode 100644 index 000000000000..8190a806086c --- /dev/null +++ b/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/local/LocalLibraryProviderSpec.scala @@ -0,0 +1,58 @@ +package org.enso.librarymanager.local + +import org.enso.editions.LibraryName +import org.scalatest.Inside +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.nio.file.Path + +class LocalLibraryProviderSpec extends AnyWordSpec with Matchers with Inside { + private val primaryPath: Path = + Path.of(getClass.getResource("test-library-path").toURI) + private val secondaryPath: Path = + Path.of(getClass.getResource("secondary-test-library-path").toURI) + + val libraryPath: List[Path] = List(primaryPath, secondaryPath) + + "LocalLibraryProvider" should { + "resolve local libraries by config name regardless of directory name" in { + val provider = new DefaultLocalLibraryProvider(libraryPath) + inside(provider.findLibrary(LibraryName("user1", "Library_1"))) { + case Some(root) => + root.location shouldEqual secondaryPath.resolve("new_folder1") + } + } + + "resolve local libraries with conflicting names, disambiguating by namespace" in { + val provider = new DefaultLocalLibraryProvider(libraryPath) + inside(provider.findLibrary(LibraryName("dev1", "Simple_Library"))) { + case Some(root) => + root.location shouldEqual primaryPath.resolve("Simple_Library_1") + } + } + + "pick alphabetically first path if there are multiple ambiguous libraries with the same name+namespace in a single directory" in { + // Ideally this should bubble-up to be a compiler warning/error in the + // import statement, but it's a lot of additional work for a rare + // occurrence, so for now we'll just log a warning - we can revisit this + // later. + val provider = new DefaultLocalLibraryProvider(libraryPath) + val ambiguousName = + LibraryName("ambiguous_developer", "Ambiguous_Library") + inside(provider.findLibrary(ambiguousName)) { case Some(root) => + val expectedPath = primaryPath.resolve("ambiguous1") + root.location shouldEqual expectedPath + } + } + + "prefer libraries in the first directory in the search path" in { + val provider = new DefaultLocalLibraryProvider(libraryPath) + inside( + provider.findLibrary(LibraryName("user123", "Library_In_Both_Dirs")) + ) { case Some(root) => + root.location shouldEqual primaryPath.resolve("library_in_both_dirs") + } + } + } +} diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala index 9a8a9f1efc86..b57e47fca470 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala @@ -90,9 +90,7 @@ object Contact { * @param name the package display name * @param normalizedName the name that will be used as a prefix to module names * of the project - * @param namespace package namespace. This field is a temporary workaround - * and will be removed with further improvements to the - * libraries system. The default value is `local`. + * @param namespace package namespace. * @param version package version * @param license package license * @param authors name and contact information of the package author(s) diff --git a/lib/scala/testkit/src/main/scala/org/enso/testkit/WithTemporaryDirectory.scala b/lib/scala/testkit/src/main/scala/org/enso/testkit/WithTemporaryDirectory.scala index 537911e57c4a..ecf3569719f2 100644 --- a/lib/scala/testkit/src/main/scala/org/enso/testkit/WithTemporaryDirectory.scala +++ b/lib/scala/testkit/src/main/scala/org/enso/testkit/WithTemporaryDirectory.scala @@ -49,7 +49,15 @@ trait WithTemporaryDirectory Thread.sleep(200) tryRemoving(retry - 1) } else { - FileUtils.forceDeleteOnExit(dir) + try { + FileUtils.forceDeleteOnExit(dir) + } catch { + case e: IOException => + System.err.println( + s"Cannot delete a temporary test directory [$dir] due to error: $e" + ) + e.printStackTrace() + } } } } diff --git a/test/Examples_Tests/package.yaml b/test/Examples_Tests/package.yaml index 4e8c3968b7b1..4570fc791ff2 100644 --- a/test/Examples_Tests/package.yaml +++ b/test/Examples_Tests/package.yaml @@ -1,4 +1,4 @@ -name: Tests +name: Examples_Tests namespace: enso_dev enso-version: default version: 0.0.1 diff --git a/test/Table_Tests/package.yaml b/test/Table_Tests/package.yaml index de5a67f65f01..d1174d2d9404 100644 --- a/test/Table_Tests/package.yaml +++ b/test/Table_Tests/package.yaml @@ -1,6 +1,7 @@ name: Table_Tests +namespace: enso_dev version: 0.0.1 -enso-version: default license: MIT author: enso-dev@enso.org maintainer: enso-dev@enso.org +prefer-local-libraries: true diff --git a/test/Table_Tests/src/Common_Table_Operations/Column_Operations_Spec.enso b/test/Table_Tests/src/Common_Table_Operations/Column_Operations_Spec.enso index 88de2ea552bc..07195c82df8f 100644 --- a/test/Table_Tests/src/Common_Table_Operations/Column_Operations_Spec.enso +++ b/test/Table_Tests/src/Common_Table_Operations/Column_Operations_Spec.enso @@ -6,7 +6,6 @@ import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import Standard.Base.Meta.Type import Standard.Database.Data.Column.Column import Standard.Database.Internal.Replace_Params.Replace_Params -import Standard.Test.Shared.Round_Spec from Standard.Table import Value_Type from Standard.Table.Data.Type.Value_Type import Bits @@ -17,6 +16,8 @@ from Standard.Database.Errors import all from Standard.Test import Test, Problems import Standard.Test.Extensions +import enso_dev.Tests.Data.Round_Spec + from project.Common_Table_Operations.Util import run_default_backend main = run_default_backend spec diff --git a/test/Tests/src/Data/Numbers_Spec.enso b/test/Tests/src/Data/Numbers_Spec.enso index 1f0611a0a807..14d12ed8046d 100644 --- a/test/Tests/src/Data/Numbers_Spec.enso +++ b/test/Tests/src/Data/Numbers_Spec.enso @@ -3,13 +3,14 @@ import Standard.Base.Errors.Common.Arithmetic_Error import Standard.Base.Errors.Common.Incomparable_Values import Standard.Base.Errors.Common.Type_Error import Standard.Base.Errors.Illegal_Argument.Illegal_Argument -import Standard.Test.Shared.Round_Spec from Standard.Base.Data.Numbers import Number_Parse_Error from Standard.Test import Test, Test_Suite import Standard.Test.Extensions +import project.Data.Round_Spec + polyglot java import java.math.BigInteger Integer.is_even self = self % 2 == 0 diff --git a/distribution/lib/Standard/Test/0.0.0-dev/src/Shared/Round_Spec.enso b/test/Tests/src/Data/Round_Spec.enso similarity index 100% rename from distribution/lib/Standard/Test/0.0.0-dev/src/Shared/Round_Spec.enso rename to test/Tests/src/Data/Round_Spec.enso