mirror of
https://github.com/enso-org/enso.git
synced 2024-11-23 08:08:34 +03:00
Resolve clashing project identifiers (#1665)
This commit is contained in:
parent
170514b9d2
commit
fde4f2d0d6
@ -5,3 +5,4 @@ akka.loglevel = "ERROR"
|
||||
akka.test.timefactor = ${?CI_TEST_TIMEFACTOR}
|
||||
akka.test.single-expect-default = 5s
|
||||
searcher.db.numThreads = 1
|
||||
searcher.db.properties.journal_mode = "memory"
|
||||
|
@ -9,6 +9,7 @@ import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@ -92,4 +93,9 @@ public class TruffleFileSystem implements FileSystem<TruffleFile> {
|
||||
public boolean isRegularFile(TruffleFile file) {
|
||||
return file.isRegularFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime getCreationTime(TruffleFile file) throws IOException {
|
||||
return file.getCreationTime();
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package org.enso.filesystem
|
||||
import scala.jdk.CollectionConverters._
|
||||
import java.io.{BufferedReader, BufferedWriter, File, IOException}
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.attribute.{BasicFileAttributes, FileTime}
|
||||
import java.util.stream
|
||||
|
||||
/** A generic specification of file operations based on an abstract notion
|
||||
@ -109,6 +110,13 @@ trait FileSystem[F] {
|
||||
*/
|
||||
def isRegularFile(file: F): Boolean
|
||||
|
||||
/** Get creation time of the file.
|
||||
*
|
||||
* @param file the file to check.
|
||||
* @return the file creation time.
|
||||
*/
|
||||
@throws[IOException]
|
||||
def getCreationTime(file: F): FileTime
|
||||
}
|
||||
|
||||
object FileSystem {
|
||||
@ -143,6 +151,8 @@ object FileSystem {
|
||||
def isDirectory: Boolean = fs.isDirectory(file)
|
||||
|
||||
def isRegularFile: Boolean = fs.isRegularFile(file)
|
||||
|
||||
def getCreationTime: FileTime = fs.getCreationTime(file)
|
||||
}
|
||||
|
||||
/** A [[File]] based implementation of [[FileSystem]].
|
||||
@ -181,5 +191,10 @@ object FileSystem {
|
||||
|
||||
override def isRegularFile(file: File): Boolean =
|
||||
Files.isRegularFile(file.toPath)
|
||||
|
||||
override def getCreationTime(file: File): FileTime =
|
||||
Files
|
||||
.readAttributes(file.toPath, classOf[BasicFileAttributes])
|
||||
.creationTime()
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.enso.projectmanager.infrastructure.repository
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.pkg.{Package, PackageManager}
|
||||
@ -57,7 +58,7 @@ class ProjectFileRepository[
|
||||
getAll().map(_.filter(predicate))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getAll(): F[ProjectRepositoryFailure, List[Project]] =
|
||||
override def getAll(): F[ProjectRepositoryFailure, List[Project]] = {
|
||||
fileSystem
|
||||
.list(storageConfig.userProjectsPath)
|
||||
.map(_.filter(_.isDirectory))
|
||||
@ -65,7 +66,11 @@ class ProjectFileRepository[
|
||||
Nil
|
||||
}
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
.flatMap(s => Traverse[List].traverse(s)(tryLoadProject).map(_.flatten))
|
||||
.flatMap { dirs =>
|
||||
Traverse[List].traverse(dirs)(tryLoadProject).map(_.flatten)
|
||||
}
|
||||
.flatMap(resolveClashingIds)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def findById(
|
||||
@ -81,23 +86,31 @@ class ProjectFileRepository[
|
||||
private def tryLoadProject(
|
||||
directory: File
|
||||
): F[ProjectRepositoryFailure, Option[Project]] = {
|
||||
val noop: F[ProjectRepositoryFailure, Option[ProjectMetadata]] =
|
||||
def noop[A]: F[ProjectRepositoryFailure, Option[A]] =
|
||||
Applicative[F].pure(None)
|
||||
for {
|
||||
pkgOpt <- loadPackage(directory)
|
||||
metaOpt <- pkgOpt.fold(noop)(_ => loadMetadata(directory))
|
||||
pkgOpt <- loadPackage(directory)
|
||||
metaOpt <- pkgOpt.fold(noop[ProjectMetadata])(_ =>
|
||||
loadMetadata(directory)
|
||||
)
|
||||
directoryCreationTime <- pkgOpt.fold(noop[FileTime])(
|
||||
getDirectoryCreationTime(_)
|
||||
.map(Some(_))
|
||||
.recoverWith(_ => noop)
|
||||
)
|
||||
} yield for {
|
||||
pkg <- pkgOpt
|
||||
meta <- metaOpt
|
||||
} yield {
|
||||
Project(
|
||||
id = meta.id,
|
||||
name = pkg.name,
|
||||
kind = meta.kind,
|
||||
created = meta.created,
|
||||
engineVersion = pkg.config.ensoVersion,
|
||||
lastOpened = meta.lastOpened,
|
||||
path = Some(directory.toString)
|
||||
id = meta.id,
|
||||
name = pkg.name,
|
||||
kind = meta.kind,
|
||||
created = meta.created,
|
||||
engineVersion = pkg.config.ensoVersion,
|
||||
lastOpened = meta.lastOpened,
|
||||
path = Some(directory.toString),
|
||||
directoryCreationTime = directoryCreationTime
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -151,6 +164,13 @@ class ProjectFileRepository[
|
||||
.blockingOp { PackageManager.Default.fromDirectory(projectPath) }
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
|
||||
private def getDirectoryCreationTime(
|
||||
pkg: Package[File]
|
||||
): F[ProjectRepositoryFailure, FileTime] =
|
||||
Sync[F]
|
||||
.blockingOp(pkg.fileSystem.getCreationTime(pkg.root))
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
|
||||
private def getPackage(
|
||||
projectPath: File
|
||||
): F[ProjectRepositoryFailure, Package[File]] =
|
||||
@ -291,4 +311,46 @@ class ProjectFileRepository[
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
|
||||
/** Resolve id clashes and return a list of projects with unique identifiers
|
||||
* by assigning random ids to clashing projects.
|
||||
*
|
||||
* @param projects the list of projects
|
||||
* @return return the list of projects with unique ids
|
||||
*/
|
||||
private def resolveClashingIds(
|
||||
projects: List[Project]
|
||||
): F[ProjectRepositoryFailure, List[Project]] = {
|
||||
val clashing = markProjectsWithClashingIds(projects)
|
||||
Traverse[List].traverse(clashing) { case (isClashing, project) =>
|
||||
if (isClashing) {
|
||||
for {
|
||||
newId <- gen.randomUUID()
|
||||
updatedProject = project.copy(id = newId)
|
||||
_ <- update(updatedProject)
|
||||
} yield updatedProject
|
||||
} else {
|
||||
Applicative[F].pure(project)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Take a list of projects and mark the projects that have duplicate ids.
|
||||
*
|
||||
* @param projects the list of projects
|
||||
* @return the list of pairs. Fist element of the pair indicates if the
|
||||
* project has clashing id.
|
||||
*/
|
||||
private def markProjectsWithClashingIds(
|
||||
projects: List[Project]
|
||||
): List[(Boolean, Project)] = {
|
||||
projects.groupBy(_.id).foldRight(List.empty[(Boolean, Project)]) {
|
||||
case ((_, groupedProjects), acc) =>
|
||||
// groupBy always returns non-empty list
|
||||
(groupedProjects.sortBy(_.directoryCreationTime): @unchecked) match {
|
||||
case project :: clashingProjects =>
|
||||
(false, project) :: clashingProjects.map((true, _)) ::: acc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.enso.projectmanager.model
|
||||
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@ -20,7 +21,8 @@ case class Project(
|
||||
name: String,
|
||||
kind: ProjectKind,
|
||||
created: OffsetDateTime,
|
||||
engineVersion: EnsoVersion = DefaultEnsoVersion,
|
||||
lastOpened: Option[OffsetDateTime] = None,
|
||||
path: Option[String] = None
|
||||
engineVersion: EnsoVersion = DefaultEnsoVersion,
|
||||
lastOpened: Option[OffsetDateTime] = None,
|
||||
path: Option[String] = None,
|
||||
directoryCreationTime: Option[FileTime] = None
|
||||
)
|
||||
|
@ -59,3 +59,5 @@ akka.loglevel = "ERROR"
|
||||
akka.test.timefactor = ${?CI_TEST_TIMEFACTOR}
|
||||
akka.test.single-expect-default = 5s
|
||||
searcher.db.numThreads = 1
|
||||
searcher.db.properties.journal_mode = "memory"
|
||||
|
||||
|
@ -445,6 +445,59 @@ class ProjectManagementApiSpec
|
||||
deleteProject(projectId)
|
||||
}
|
||||
|
||||
"deduplicate project ids" taggedAs Flaky in {
|
||||
val projectName1 = "Foo"
|
||||
|
||||
implicit val client = new WsTestClient(address)
|
||||
|
||||
// given
|
||||
val projectId1 = createProject(projectName1)
|
||||
val projectDir1 = new File(userProjectDir, projectName1)
|
||||
val projectDir2 = new File(userProjectDir, "Test")
|
||||
FileUtils.copyDirectory(projectDir1, projectDir2)
|
||||
|
||||
// when
|
||||
testClock.moveTimeForward()
|
||||
openProject(projectId1)
|
||||
val projectOpenTime = testClock.currentTime
|
||||
//then
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "project/list",
|
||||
"id": 0,
|
||||
"params": { }
|
||||
}
|
||||
""")
|
||||
val projectId2 = getGeneratedUUID
|
||||
client.expectJson(json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
"id":0,
|
||||
"result": {
|
||||
"projects": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"id": $projectId1,
|
||||
"engineVersion": $engineToInstall,
|
||||
"lastOpened": $projectOpenTime
|
||||
},
|
||||
{
|
||||
"name": "Foo",
|
||||
"id": $projectId2,
|
||||
"engineVersion": $engineToInstall,
|
||||
"lastOpened": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// teardown
|
||||
closeProject(projectId1)
|
||||
deleteProject(projectId1)
|
||||
deleteProject(projectId2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"project/close" must {
|
||||
@ -615,6 +668,56 @@ class ProjectManagementApiSpec
|
||||
deleteProject(bazId)
|
||||
}
|
||||
|
||||
"resolve clashing ids" taggedAs Flaky in {
|
||||
val projectName1 = "Foo"
|
||||
|
||||
implicit val client = new WsTestClient(address)
|
||||
|
||||
// given
|
||||
val projectId1 = createProject(projectName1)
|
||||
val projectDir1 = new File(userProjectDir, projectName1)
|
||||
val projectDir2 = new File(userProjectDir, "Test")
|
||||
FileUtils.copyDirectory(projectDir1, projectDir2)
|
||||
|
||||
// when
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "project/list",
|
||||
"id": 0,
|
||||
"params": { }
|
||||
}
|
||||
""")
|
||||
|
||||
//then
|
||||
val projectId2 = getGeneratedUUID
|
||||
client.expectJson(json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
"id":0,
|
||||
"result": {
|
||||
"projects": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"id": $projectId1,
|
||||
"engineVersion": $engineToInstall,
|
||||
"lastOpened": null
|
||||
},
|
||||
{
|
||||
"name": "Foo",
|
||||
"id": $projectId2,
|
||||
"engineVersion": $engineToInstall,
|
||||
"lastOpened": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// teardown
|
||||
deleteProject(projectId1)
|
||||
deleteProject(projectId2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"project/rename" must {
|
||||
|
Loading…
Reference in New Issue
Block a user