Skip to content

Commit 4fb7e24

Browse files
committed
fix(mps-sync-plugin): skip read-only models when syncing to MPS
1 parent ae3bb1e commit 4fb7e24

File tree

48 files changed

+1395
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1395
-2
lines changed

bulk-model-sync-lib/src/commonMain/kotlin/org/modelix/model/sync/bulk/ModelSynchronizer.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ class ModelSynchronizer(
122122
targetNode: IWritableNode,
123123
forceSyncDescendants: Boolean,
124124
) {
125+
if (targetNode.isReadOnly()) {
126+
LOG.trace { "Skipping read-only node. targetNode = $targetNode" }
127+
return
128+
}
125129
LOG.trace { "Synchronizing changed node. sourceNode = $sourceNode" }
126130
runSafe { synchronizeProperties(sourceNode, targetNode) }
127131
runSafe { synchronizeReferences(sourceNode, targetNode) }

model-api/src/commonMain/kotlin/org/modelix/model/api/IWritableNode.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ fun INode.getOriginalOrCurrentReference(): String = getOriginalReference() ?: re
4848

4949
interface IWritableNode : IReadableNode {
5050
override fun asLegacyNode(): INode = WritableNodeAsLegacyNode(this)
51+
fun isReadOnly(): Boolean = false
5152

5253
override fun getModel(): IMutableModel
5354
override fun getAllChildren(): List<IWritableNode>

mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSModelAsNode.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ data class MPSModelAsNode(val model: SModel) : MPSGenericNodeAdapter<SModel>() {
190190

191191
override fun getChildAccessors() = childAccessors
192192

193+
override fun isReadOnly(): Boolean {
194+
return model.isReadOnly
195+
}
196+
193197
override fun getParent(): IWritableNode? {
194198
return model.module?.let { MPSModuleAsNode(it) }
195199
}

mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSModuleAsNode.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ abstract class MPSModuleAsNode<E : SModule> : MPSGenericNodeAdapter<E>() {
283283

284284
abstract val module: E
285285

286+
override fun isReadOnly(): Boolean {
287+
return module.isReadOnly
288+
}
289+
286290
override fun getElement(): E {
287291
return module
288292
}

mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSWritableNode.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ fun SNode.asReadableNode(): IReadableNode = MPSWritableNode(this)
3737
fun SNode.asWritableNode(): IWritableNode = MPSWritableNode(this)
3838

3939
data class MPSWritableNode(val node: SNode) : IWritableNode, ISyncTargetNode {
40+
41+
override fun isReadOnly(): Boolean {
42+
return node.model?.isReadOnly == true
43+
}
44+
4045
override fun getModel(): IMutableModel {
4146
return MPSArea(node.model?.repository ?: MPSModuleRepository.getInstance()).asModel()
4247
}

mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSnapshot.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ private fun Project.captureFileContents(): Map<String, String> {
4848
module as AbstractModule
4949
module.save()
5050
for (model in module.models.filterIsInstance<EditableSModel>()) {
51+
if (model.isReadOnly) continue
5152
ModelixMpsApi.forceSave(model)
5253
}
5354
}

mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSyncTest.kt

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
package org.modelix.mps.sync3
22

33
import com.intellij.configurationStore.saveSettings
4-
import com.intellij.openapi.util.use
54
import com.intellij.openapi.application.ApplicationInfo
65
import io.ktor.client.HttpClient
76
import io.ktor.client.engine.cio.CIO
87
import io.ktor.client.plugins.ResponseException
98
import io.ktor.client.request.get
109
import io.ktor.http.HttpStatusCode
10+
import jetbrains.mps.core.tool.environment.util.SetLibraryContributor
11+
import jetbrains.mps.ide.MPSCoreComponents
1112
import jetbrains.mps.ide.project.ProjectHelper
13+
import jetbrains.mps.library.contributor.LibDescriptor
14+
import jetbrains.mps.smodel.MPSModuleRepository
15+
import jetbrains.mps.smodel.SNodePointer
1216
import jetbrains.mps.smodel.SNodeUtil
1317
import jetbrains.mps.smodel.adapter.ids.SConceptId
1418
import jetbrains.mps.smodel.adapter.ids.SContainmentLinkId
1519
import jetbrains.mps.smodel.adapter.structure.concept.SConceptAdapterById
1620
import jetbrains.mps.smodel.adapter.structure.link.SContainmentLinkAdapterById
21+
import jetbrains.mps.vfs.VFSManager
1722
import kotlinx.coroutines.delay
1823
import org.jetbrains.mps.openapi.model.SNode
1924
import org.jetbrains.mps.openapi.persistence.PersistenceFacade
@@ -26,6 +31,7 @@ import org.modelix.model.IVersion
2631
import org.modelix.model.api.BuiltinLanguages
2732
import org.modelix.model.api.INodeReference
2833
import org.modelix.model.api.getDescendants
34+
import org.modelix.model.api.getName
2935
import org.modelix.model.client2.ModelClientV2
3036
import org.modelix.model.client2.computeWriteOnModel
3137
import org.modelix.model.client2.runWriteOnModel
@@ -39,15 +45,18 @@ import org.modelix.model.lazy.RepositoryId
3945
import org.modelix.model.mpsadapters.MPSModuleAsNode
4046
import org.modelix.model.mpsadapters.MPSProjectReference
4147
import org.modelix.model.mpsadapters.MPSProperty
48+
import org.modelix.model.mpsadapters.computeRead
4249
import org.modelix.model.mpsadapters.toModelix
4350
import org.modelix.model.mutable.asModelSingleThreaded
4451
import org.modelix.model.oauth.IAuthConfig
4552
import org.modelix.model.operations.IOperation
53+
import org.modelix.model.operations.SetPropertyOp
4654
import org.modelix.model.server.ModelServerPermissionSchema
47-
import org.modelix.mps.api.ModelixMpsApi
4855
import org.modelix.mps.multiplatform.model.MPSIdGenerator
56+
import org.modelix.mps.multiplatform.model.MPSNodeReference
4957
import org.modelix.mps.sync3.ui.OpenFrontendAction
5058
import org.modelix.streams.getBlocking
59+
import java.io.File
5160
import java.net.URL
5261
import java.nio.file.Path
5362
import java.util.concurrent.atomic.AtomicLong
@@ -530,6 +539,81 @@ class ProjectSyncTest : MPSTestBase() {
530539
assertEquals(version1.getContentHash(), version2.getContentHash())
531540
}
532541

542+
private suspend fun <R> withGlobalLibrary(folder: File, body: suspend () -> R): R {
543+
val vfsManager = MPSCoreComponents.getInstance().platform.findComponent(VFSManager::class.java)!!
544+
val packagedModulesFolder = vfsManager.getFileSystem(VFSManager.FILE_FS).getFile(folder)
545+
val packagedModulesContributors = listOf(SetLibraryContributor.fromSet("packaged-modules", setOf(LibDescriptor(packagedModulesFolder))))
546+
MPSCoreComponents.getInstance().libraryInitializer.load(packagedModulesContributors)
547+
try {
548+
return body()
549+
} finally {
550+
MPSCoreComponents.getInstance().libraryInitializer.unload(packagedModulesContributors)
551+
}
552+
}
553+
554+
fun `test change in packaged module`(): Unit = runWithModelServer { port ->
555+
// Modules in packaged-modules/modules.jar are the same as in solutions/ but packaged into a jar file
556+
// which makes them read-only.
557+
558+
val branchRef = RepositoryId("sync-test").getBranchReference("branchA")
559+
val changedNodeRef = SNodePointer.deserialize("r:cd78e6ac-0e34-490a-9b49-e5643f948d6d(NewSolution.a_model)/8281020627045237343")
560+
val version1 = withGlobalLibrary(File("testdata/with-packaged-module/packaged-modules")) {
561+
val globalRepo = MPSModuleRepository.getInstance()
562+
globalRepo.modelAccess.runReadAction {
563+
assertContains(globalRepo.modules.map { it.moduleName.orEmpty() }.filterNot { it.startsWith("jetbrains.") }, "NewSolution")
564+
}
565+
566+
openTestProject("with-packaged-module").also { p ->
567+
val mpsProject = ProjectHelper.fromIdeaProject(p)!!
568+
mpsProject.modelAccess.runReadAction {
569+
val changedNode = checkNotNull(changedNodeRef.resolve(mpsProject.repository)) {
570+
"Node not found: $changedNodeRef"
571+
}
572+
assertEquals("MyClass", changedNode.name)
573+
assertEquals("NewSolution.a_model", changedNode.model?.name?.value)
574+
assertTrue("Not read-only: ${changedNode.model?.name}", changedNode.model?.isReadOnly == true)
575+
}
576+
p.close()
577+
}
578+
579+
syncProjectToServer("with-packaged-module", port, branchRef)
580+
}
581+
582+
delay(1.seconds) // wait for the modules to be unregistered
583+
584+
val version2 = syncProjectToServer("with-packaged-module-changed", port, branchRef, version1.getContentHash())
585+
586+
// read-only modules should be skipped
587+
assertNotEquals(version1.getContentHash(), version2.getContentHash())
588+
version2 as CLVersion
589+
val operation = version2.operations.single() as SetPropertyOp
590+
assertEquals("MyClassRenamed", operation.value)
591+
592+
val branchRef2 = RepositoryId("sync-test").getBranchReference("branchB")
593+
val client = ModelClientV2.builder().url("http://localhost:$port").lazyAndBlockingQueries().build()
594+
client.push(branchRef2, version1, null)
595+
596+
withGlobalLibrary(File("testdata/with-packaged-module/packaged-modules")) {
597+
openTestProject("with-packaged-module")
598+
val binding = IModelSyncService.getInstance(mpsProject)
599+
.addServer("http://localhost:$port")
600+
.bind(branchRef, null)
601+
binding.flush()
602+
603+
suspend fun nameOnServer() = client.pull(branchRef2, null).getModelTree().asModelSingleThreaded().resolveNode(MPSNodeReference.parseSNodeReference(changedNodeRef.toString())).getName()
604+
fun nameInMPS() = mpsProject.repository.computeRead { changedNodeRef.resolve(mpsProject.repository)!!.name }
605+
606+
assertEquals("MyClass", nameInMPS())
607+
MPSNodeReference.parseSNodeReference(changedNodeRef.toString())
608+
assertEquals("MyClass", nameOnServer())
609+
client.push(branchRef2, version2, null)
610+
assertEquals("MyClassRenamed", nameOnServer())
611+
binding.flush()
612+
assertEquals("MyClass", nameInMPS()) // name remains unchanged, because the model is read-only
613+
assertEquals("MyClassRenamed", nameOnServer())
614+
}
615+
}
616+
533617
fun `test missing permission on initial sync is detected`(): Unit = runWithModelServer(hmacKey = "abc") { port ->
534618
val branchRef = RepositoryId("sync-test").getBranchReference("branchA")
535619
AppLevelModelSyncService.getInstance().getOrCreateConnection(
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Default ignored files
2+
/shelf/
3+
/workspace.xml
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project version="4">
3+
<component name="MigrationProperties">
4+
<entry key="project.baseline.version" value="211" />
5+
</component>
6+
</project>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project version="4">
3+
<component name="MPSProject">
4+
<projectModules>
5+
<modulePath path="$PROJECT_DIR$/devkits/NewDevkit/NewDevkit.devkit" folder="" />
6+
<modulePath path="$PROJECT_DIR$/languages/NewLanguage/NewLanguage.mpl" folder="" />
7+
<modulePath path="$PROJECT_DIR$/solutions/NewRuntimeSolution/NewRuntimeSolution.msd" folder="" />
8+
<modulePath path="$PROJECT_DIR$/solutions/NewSolution/NewSolution.msd" folder="" />
9+
<modulePath path="$PROJECT_DIR$/solutions/ToBeDeleted/ToBeDeleted.msd" folder="" />
10+
</projectModules>
11+
</component>
12+
</project>

0 commit comments

Comments
 (0)