diff --git a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/INodeUtils.kt b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/INodeUtils.kt index f8e31af8..04092f8e 100644 --- a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/INodeUtils.kt +++ b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/INodeUtils.kt @@ -1,9 +1,6 @@ package org.modelix.model.mpsplugin -import jetbrains.mps.baseLanguage.tuples.runtime.MultiTuple -import jetbrains.mps.baseLanguage.tuples.runtime.Tuples import jetbrains.mps.internal.collections.runtime.ListSequence -import jetbrains.mps.internal.collections.runtime.MapSequence import jetbrains.mps.internal.collections.runtime.Sequence import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory import org.jetbrains.mps.openapi.language.SConcept @@ -14,7 +11,6 @@ import org.modelix.model.api.INode import org.modelix.model.api.IProperty import org.modelix.model.api.IReferenceLink import org.modelix.model.api.PNodeAdapter -import org.modelix.model.api.PNodeAdapter.Companion.wrap import org.modelix.model.mpsadapters.mps.SConceptAdapter import java.util.LinkedList import java.util.Objects @@ -115,77 +111,84 @@ object INodeUtils { } } - fun copyPropertyIfNecessary(_this: INode, original: INode, property: SProperty) { - if (Objects.equals(original.getPropertyValue(property.name), _this.getPropertyValue(property.name))) { + fun copyPropertyIfNecessary(copiedNode: INode, originalNode: INode, property: SProperty) { + if (Objects.equals(originalNode.getPropertyValue(property.name), copiedNode.getPropertyValue(property.name))) { return } - copyProperty(_this, original, property) + copyProperty(copiedNode, originalNode, property) } - fun replicateChild(_this: INode?, role: String, original: INode?): INode { - try { - val equivalenceMap: Map = MapSequence.fromMap(HashMap()) - val postponedReferencesAssignments: List> = - ListSequence.fromList(LinkedList()) - val result: INode = - replicateChildHelper(_this, role, original, equivalenceMap, postponedReferencesAssignments) - for (postponedRefAssignment: Tuples._3 in ListSequence.fromList( - postponedReferencesAssignments, - )) { - var target: INode? = postponedRefAssignment._2() - if (MapSequence.fromMap(equivalenceMap).containsKey(target)) { - target = MapSequence.fromMap(equivalenceMap).get(target) - } - postponedRefAssignment._0().setReferenceTarget(postponedRefAssignment._1(), target) + fun replicateChild(copiedParent: INode, role: String, originalChild: INode): INode { + return replicateChildren(copiedParent, role, listOf(originalChild)).single() + } + + data class PostponedReferenceAssignments( + val copiedNode: INode, + val referenceRole: String, + val originalTarget: INode, + ) + + private fun replicateChildren(copiedParent: INode, role: String, originalChildren: Iterable): List { + val equivalenceMap = mutableMapOf() + val postponedReferencesAssignments = mutableListOf() + val resultNodes = mutableListOf() + for (originalChild in originalChildren) { + try { + val result = replicateChildHelper(copiedParent, role, originalChild, equivalenceMap, postponedReferencesAssignments) + resultNodes.add(result) + } catch (e: Exception) { + throw RuntimeException( + "Unable to replicate child in role " + role + ". Original child: " + originalChild + ", Copied parent: " + copiedParent, + e, + ) } - return result - } catch (e: Exception) { - throw RuntimeException( - "Unable to replicate child in role " + role + ". Original: " + original + ", This: " + _this, - e, - ) } + for (postponedRefAssignment in postponedReferencesAssignments) { + val copiedNode = postponedRefAssignment.copiedNode + val referenceRole = postponedRefAssignment.referenceRole + val copiedTarget = equivalenceMap[postponedRefAssignment.originalTarget] + val targetToSet = copiedTarget ?: postponedRefAssignment.originalTarget + copiedNode.setReferenceTarget(referenceRole, targetToSet) + } + return resultNodes } - fun cloneChildren(_this: INode?, original: INode?, role: String) { - removeAllChildrenWithRole(_this, role) - for (originalChild: INode? in Sequence.fromIterable( - original!!.getChildren(role), - )) { - replicateChild(_this, role, originalChild) - } + fun cloneChildren(copiedParent: INode, originalParent: INode, role: String) { + removeAllChildrenWithRole(copiedParent, role) + replicateChildren(copiedParent, role, originalParent.getChildren(role)) } fun replicateChildHelper( - _this: INode?, + copiedParent: INode, role: String, - original: INode?, - equivalenceMap: Map?, - postponedReferencesAssignments: List>?, + original: INode, + equivalenceMap: MutableMap, + postponedReferencesAssignments: MutableList, ): INode { - val concept: IConcept? = original!!.concept - var copy: INode? = null - try { - copy = _this!!.addNewChild(role, -1, concept) + val concept: IConcept? = original.concept + checkNotNull(concept) + val copy = try { + val addedChild = copiedParent.addNewChild(role, -1, concept) + equivalenceMap[original] = addedChild + addedChild } catch (e: Exception) { throw RuntimeException( - "Unable to add child to " + _this + " with role " + role + " and concept " + concept, + "Unable to add child to " + copiedParent + " with role " + role + " and concept " + concept, e, ) } - for (property: IProperty in ListSequence.fromList(concept!!.getAllProperties())) { + for (property: IProperty in concept.getAllProperties()) { copy.setPropertyValue(property.name, original.getPropertyValue(property.name)) } - for (childLink: IChildLink in ListSequence.fromList(concept.getAllChildLinks())) { - for (child: INode? in ListSequence.fromList(getChidlrenAsList(original, childLink.name))) { + for (childLink: IChildLink in concept.getAllChildLinks()) { + for (child: INode in getChidlrenAsList(original, childLink.name)) { replicateChildHelper(copy, childLink.name, child, equivalenceMap, postponedReferencesAssignments) } } - for (refLink: IReferenceLink in ListSequence.fromList(concept.getAllReferenceLinks())) { - val target: INode? = original.getReferenceTarget(refLink.name) - if (target != null) { - ListSequence.fromList(postponedReferencesAssignments) - .addElement(MultiTuple.from(copy, refLink.name, target)) + for (refLink: IReferenceLink in concept.getAllReferenceLinks()) { + val originalTarget: INode? = original.getReferenceTarget(refLink.name) + if (originalTarget != null) { + postponedReferencesAssignments.add(PostponedReferenceAssignments(copy, refLink.name, originalTarget)) } } return copy diff --git a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/ModelCloudImportUtils.kt b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/ModelCloudImportUtils.kt index ea694430..241e5341 100644 --- a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/ModelCloudImportUtils.kt +++ b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/ModelCloudImportUtils.kt @@ -3,7 +3,6 @@ package org.modelix.model.mpsplugin import jetbrains.mps.baseLanguage.closures.runtime.Wrappers._T import jetbrains.mps.ide.project.ProjectHelper import jetbrains.mps.internal.collections.runtime.ListSequence -import jetbrains.mps.internal.collections.runtime.Sequence import jetbrains.mps.progress.EmptyProgressMonitor import jetbrains.mps.project.MPSProject import jetbrains.mps.project.Project @@ -14,18 +13,14 @@ import org.jetbrains.mps.openapi.language.SContainmentLink import org.jetbrains.mps.openapi.language.SProperty import org.jetbrains.mps.openapi.model.SModel import org.jetbrains.mps.openapi.model.SNode -import org.jetbrains.mps.openapi.model.SReference import org.jetbrains.mps.openapi.module.SModule import org.jetbrains.mps.openapi.util.ProgressMonitor import org.modelix.model.api.INode import org.modelix.model.api.PNodeAdapter -import org.modelix.model.api.PNodeAdapter.Companion.wrap import org.modelix.model.lazy.RepositoryId import org.modelix.model.mpsadapters.mps.NodeToSNodeAdapter -import org.modelix.model.mpsadapters.mps.SConceptAdapter import org.modelix.model.mpsadapters.mps.SModelAsNode import org.modelix.model.mpsadapters.mps.SModuleAsNode -import org.modelix.model.mpsadapters.mps.SNodeToNodeAdapter import org.modelix.model.mpsplugin.history.CloudNodeTreeNode import org.modelix.model.mpsplugin.history.CloudNodeTreeNodeBinding import org.modelix.model.mpsplugin.plugin.PersistedBindingConfiguration @@ -114,7 +109,7 @@ object ModelCloudImportUtils { progress: ProgressMonitor?, ): INode? { // First create the module - val cloudModuleNode: INode? = treeInRepository.createModule(module!!.moduleName) + val cloudModuleNode: INode = treeInRepository.createModule(module!!.moduleName) replicatePhysicalModule(treeInRepository, cloudModuleNode, module, null, progress) return cloudModuleNode } @@ -172,8 +167,8 @@ object ModelCloudImportUtils { */ fun replicatePhysicalModule( treeInRepository: CloudRepository, - cloudModule: INode?, - physicalModule: SModule?, + cloudModule: INode, + physicalModule: SModule, modelMappingConsumer: Consumer?, progress: ProgressMonitor?, ) { @@ -181,7 +176,7 @@ object ModelCloudImportUtils { if (_progress.value == null) { _progress.value = EmptyProgressMonitor() } - val sModuleAsNode: SModuleAsNode? = SModuleAsNode.Companion.wrap(physicalModule) + val sModuleAsNode = SModuleAsNode(physicalModule) treeInRepository.runWrite(object : Consumer { override fun accept(rootNode: PNodeAdapter?) { INodeUtils.copyProperty(cloudModule, sModuleAsNode, PROPS.`name$MnvL`.name) @@ -227,8 +222,8 @@ object ModelCloudImportUtils { * * @return the created cloud model */ - fun copyPhysicalModel(treeInRepository: CloudRepository, cloudModule: INode?, physicalModel: SModel?): INode? { - val originalModel: INode? = SModelAsNode.Companion.wrap(physicalModel) + fun copyPhysicalModel(treeInRepository: CloudRepository, cloudModule: INode?, physicalModel: SModel): INode { + val originalModel = SModelAsNode(physicalModel) val cloudModel: INode = treeInRepository.createNode( cloudModule, LINKS.`models$h3QT`, @@ -239,46 +234,13 @@ object ModelCloudImportUtils { INodeUtils.copyProperty(cloudModel, originalModel, PROPS.`id$lDUo`.name) INodeUtils.cloneChildren(cloudModel, originalModel, LINKS.`modelImports$8DOI`.name) INodeUtils.cloneChildren(cloudModel, originalModel, LINKS.`usedLanguages$QK4E`.name) - for (physicalRoot: SNode in Sequence.fromIterable( - physicalModel!!.rootNodes, - )) { - val cloudRoot: INode = cloudModel.addNewChild( - LINKS.`rootNodes$jxXY`.name, - -1, - SConceptAdapter.Companion.wrap(physicalRoot.concept), - ) - replicatePhysicalNode(cloudRoot, physicalRoot) - } + INodeUtils.cloneChildren(cloudModel, originalModel, LINKS.`rootNodes$jxXY`.name) } }, ) return cloudModel } - /** - * This takes a cloud node already created and a physical node. - * It then ensures that the cloud node is exactly as the original physical node. - * - * It operates recursively on children. - */ - private fun replicatePhysicalNode(cloudNode: INode, physicalNode: SNode) { - MPSNodeMapping.mapToMpsNode(cloudNode, physicalNode) - for (prop: SProperty in Sequence.fromIterable(physicalNode.properties)) { - cloudNode.setPropertyValue(prop.name, physicalNode.getProperty(prop)) - } - for (ref: SReference in Sequence.fromIterable(physicalNode.references)) { - cloudNode.setReferenceTarget(ref.role, SNodeToNodeAdapter.Companion.wrap(ref.targetNode)) - } - for (physicalChild: SNode in Sequence.fromIterable(physicalNode.children)) { - val cloudChild: INode = cloudNode.addNewChild( - physicalChild.containmentLink!!.name, - -1, - SConceptAdapter.Companion.wrap(physicalChild.concept), - ) - replicatePhysicalNode(cloudChild, physicalChild) - } - } - private object PROPS { /*package*/ val `name$MnvL`: SProperty = MetaAdapterFactory.getProperty( diff --git a/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncService.kt b/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncService.kt index d631587d..8a1ca3ea 100644 --- a/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncService.kt +++ b/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncService.kt @@ -113,7 +113,7 @@ class ModelSyncService : Disposable, ISyncService { private fun getRootBindings() = ModelServerConnections.instance.modelServers .flatMap(ModelServerConnection::getRootBindings) - private fun flushAllBindings() { + override fun flushAllBindings() { getRootBindings().forEach { it.syncQueue.flush() } } diff --git a/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/api/ISyncService.kt b/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/api/ISyncService.kt index 0fcb9078..e3ee582b 100644 --- a/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/api/ISyncService.kt +++ b/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/api/ISyncService.kt @@ -62,6 +62,15 @@ interface ISyncService { * To not deadlock your program, do not call this method while holding a write lock or read lock. */ fun findMpsNode(cloudNodeReference: INodeReference): List + + /** + * Synchronize all bindings between MPS and the model server. + * The call blocks until all synchronizations are finished or timeout. + * + * The synchronization is always scheduled on a different thread which will try to acquire a write and read lock. + * To not deadlock your program, do not call this method while holding a write lock or read lock. + */ + fun flushAllBindings() } interface IModelServerConnection : Disposable { diff --git a/mps-legacy-sync-plugin/src/test/kotlin/FindingNodesInBindingTest.kt b/mps-legacy-sync-plugin/src/test/kotlin/FindingNodesInBindingTest.kt index aea7eab4..eac52239 100644 --- a/mps-legacy-sync-plugin/src/test/kotlin/FindingNodesInBindingTest.kt +++ b/mps-legacy-sync-plugin/src/test/kotlin/FindingNodesInBindingTest.kt @@ -15,7 +15,6 @@ */ import io.ktor.http.Url -import kotlinx.coroutines.delay import org.jetbrains.mps.openapi.language.SConcept import org.jetbrains.mps.openapi.model.SNode import org.modelix.model.api.PNodeReference @@ -80,9 +79,9 @@ class FindingNodesInBindingTest : SyncPluginTestBase("projectWithOneEmptyModel") } fun `test can find cloud nodes and MPS nodes when connection by legacy API`() = runTestWithSyncService { syncService -> - // Using ModelCloudImportUtils.copyAndSyncInModelixAsIndependentModule - // instead of IBranchConnection.bindModule is the legacy API - // The old API must be supported as it is used in actions like CopyAndSyncPhysicalModuleOnCloud_Action + // Using the legacy API ModelCloudImportUtils.copyAndSyncInModelixAsIndependentModule + // instead of IBranchConnection.bindModule. + // The legacy API must be supported as it is used in actions like CopyAndSyncPhysicalModuleOnCloud_Action // Arrange: Create data val classConcept = resolveMPSConcept("jetbrains.mps.baseLanguage.ClassConcept") @@ -131,15 +130,4 @@ class FindingNodesInBindingTest : SyncPluginTestBase("projectWithOneEmptyModel") assertEquals(newRootNode, mpsNodeFromFirstSecondCloudNode) assertEquals(mpsNodeFromFirstCloudNode, mpsNodeFromFirstSecondCloudNode) } - - private suspend fun delayUntil(condition: () -> Boolean) { - var remainingDelays = 30 - while (!condition()) { - delay(1000) - remainingDelays-- - if (remainingDelays == 0) { - throw IllegalStateException("Waited too long.") - } - } - } } diff --git a/mps-legacy-sync-plugin/src/test/kotlin/ReferenceSynchronisationTest.kt b/mps-legacy-sync-plugin/src/test/kotlin/ReferenceSynchronisationTest.kt new file mode 100644 index 00000000..b19caeb5 --- /dev/null +++ b/mps-legacy-sync-plugin/src/test/kotlin/ReferenceSynchronisationTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.ktor.http.Url +import org.modelix.model.api.PNodeReference +import org.modelix.model.data.NodeData +import org.modelix.model.lazy.filterNotNullValues +import org.modelix.model.mpsplugin.ModelCloudImportUtils +import org.modelix.model.mpsplugin.ModelServerConnections + +class ReferenceSynchronisationTest : SyncPluginTestBase("projectWithReferences") { + + private fun assertAllReferencesArePNodeReferences(nodeData: NodeData) { + for ((referenceRole, referenceValue) in nodeData.references) { + val reference = PNodeReference.tryDeserialize(referenceValue) + assertNotNull( + "Reference is not a PNodeReference. [node=${nodeData.id}] [role=$referenceRole] [reference=$referenceValue]", + reference, + ) + } + nodeData.children.forEach(::assertAllReferencesArePNodeReferences) + } + + private fun countSetReferences(nodeData: NodeData): Int = + nodeData.references.filterNotNullValues().size + nodeData.children.map(::countSetReferences).sum() + + private fun assertNumberOfSetReferences(expectedNumberOfSetReferences: Int, nodeData: NodeData) { + val actualNumberSetOfReferences = countSetReferences(nodeData) + assertEquals(expectedNumberOfSetReferences, actualNumberSetOfReferences) + } + + fun `test references are initially synced as modelix references in module`() = + runTestWithSyncService { syncService -> + // Arrange + val existingSolution = mpsProject.projectModules.single() + + // Act + val moduleBinding = syncService.connectServer(httpClient, Url("http://localhost/")) + .newBranchConnection(defaultBranchRef) + .bindModule(existingSolution, null) + moduleBinding.flush() + + // Assert + val rootNodeData = readDumpFromServer(defaultBranchRef) + // The model has exactly two references. + assertNumberOfSetReferences(2, rootNodeData) + assertAllReferencesArePNodeReferences(rootNodeData) + } + + fun `test references are initially synced as modelix references in module when connecting by legacy API`() = + runTestWithSyncService { syncService -> + // Using the legacy API ModelCloudImportUtils.copyAndSyncInModelixAsIndependentModule + // instead of IBranchConnection.bindModule. + // The legacy API must be supported as it is used in actions like CopyAndSyncPhysicalModuleOnCloud_Action + + // Arrange + val existingSolution = mpsProject.projectModules.single() + syncService.connectServer(httpClient, Url("http://localhost/")) + val modelServer = ModelServerConnections.instance.modelServers.single() + // Arrange: Setup bindings + // after `info` is fetched, we can start adding repositories + delayUntil { modelServer.info != null } + delayUntil { + ModelServerConnections.instance.connectedTreesInRepositories + .map { it.branch.getId() } + .containsAll(listOf(defaultBranchRef.repositoryId.id)) + } + val repositoryTree = ModelServerConnections.instance.connectedTreesInRepositories + .single { it.branch.getId() == defaultBranchRef.repositoryId.id } + + // Act + writeAction { + ModelCloudImportUtils.copyAndSyncInModelixAsIndependentModule( + repositoryTree, + existingSolution, + project, + null, + ) + } + syncService.flushAllBindings() + + // Assert + val rootNodeData = readDumpFromServer(defaultBranchRef) + // The model has exactly two references. + assertNumberOfSetReferences(2, rootNodeData) + assertAllReferencesArePNodeReferences(rootNodeData) + } +} diff --git a/mps-legacy-sync-plugin/src/test/kotlin/SyncPluginTestBase.kt b/mps-legacy-sync-plugin/src/test/kotlin/SyncPluginTestBase.kt index a91df6ff..b092cac8 100644 --- a/mps-legacy-sync-plugin/src/test/kotlin/SyncPluginTestBase.kt +++ b/mps-legacy-sync-plugin/src/test/kotlin/SyncPluginTestBase.kt @@ -33,6 +33,7 @@ import jetbrains.mps.library.contributor.LibDescriptor import jetbrains.mps.project.MPSProject import jetbrains.mps.smodel.adapter.structure.concept.InvalidConcept import jetbrains.mps.smodel.language.LanguageRegistry +import kotlinx.coroutines.delay import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.jetbrains.mps.openapi.language.SAbstractConcept @@ -64,6 +65,30 @@ import kotlin.io.path.absolutePathString @Suppress("removal") abstract class SyncPluginTestBase(private val testDataName: String?) : HeavyPlatformTestCase() { + + companion object { + suspend fun delayUntil( + checkIntervalMilliseconds: Long = 1000, + timeoutMilliseconds: Long = 30_000, + condition: () -> Boolean, + ) { + check(checkIntervalMilliseconds > 0) { + "checkIntervalMilliseconds must be positive." + } + check(timeoutMilliseconds > 0) { + "timeoutMilliseconds must be positive." + } + var remainingDelays = timeoutMilliseconds / checkIntervalMilliseconds + while (!condition()) { + if (remainingDelays == 0L) { + throw IllegalStateException("Waited too long.") + } + delay(checkIntervalMilliseconds) + remainingDelays-- + } + } + } + protected val baseUrl = "http://localhost/v2/" protected lateinit var httpClient: HttpClient protected lateinit var syncService: ISyncService diff --git a/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/.gitignore b/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/migration.xml b/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/migration.xml new file mode 100644 index 00000000..d512ce45 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/migration.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/modules.xml b/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/modules.xml new file mode 100644 index 00000000..f31cd531 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/vcs.xml b/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/vcs.xml new file mode 100644 index 00000000..fbbc5665 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithReferences/.mps/vcs.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/mps-legacy-sync-plugin/testdata/projectWithReferences/solutions/NewSolution/NewSolution.msd b/mps-legacy-sync-plugin/testdata/projectWithReferences/solutions/NewSolution/NewSolution.msd new file mode 100644 index 00000000..ed0337eb --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithReferences/solutions/NewSolution/NewSolution.msd @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mps-legacy-sync-plugin/testdata/projectWithReferences/solutions/NewSolution/models/NewSolution.a_model.mps b/mps-legacy-sync-plugin/testdata/projectWithReferences/solutions/NewSolution/models/NewSolution.a_model.mps new file mode 100644 index 00000000..0915e6d3 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithReferences/solutions/NewSolution/models/NewSolution.a_model.mps @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +