diff options
author | Christian W. Damus | 2017-11-22 21:31:40 +0000 |
---|---|---|
committer | Christian W. Damus | 2017-11-30 14:04:44 +0000 |
commit | 2270c96ad19452908d6c122bfd12a47fec32ae7f (patch) | |
tree | 381a075f2a94598ff0c790aeeecef1cfce3f3b31 /plugins | |
parent | 8418b7f1e115d78cc6ab6b9099425a77bbaa0ed6 (diff) | |
download | org.eclipse.papyrus-collaborativemodeling-2270c96ad19452908d6c122bfd12a47fec32ae7f.tar.gz org.eclipse.papyrus-collaborativemodeling-2270c96ad19452908d6c122bfd12a47fec32ae7f.tar.xz org.eclipse.papyrus-collaborativemodeling-2270c96ad19452908d6c122bfd12a47fec32ae7f.zip |
Bug 527638 - Support diagram migration for Papyrus models
Solves the problem when you try to compare a model from a previous
version of Papyrus.
Solved by using the reconcile mechanism of Papyrus.
Ported from EMF Compare project change https://git.eclipse.org/r/#/c/84294 with changes:
- ensure that migration of multiple sides causes no spurious conflicts
(detected by numerous failures in Git tests)
- remove Mars compatibility no longer needed
- add JUnit tests
- other minor touches
- support maven build on Mac platform
Change-Id: I2925d9ac70a6cc421be9ba9ba7f5d6906b1aa1af
Signed-off-by: Simon Delisle <simon.delisle@ericsson.com>
Also-by: Christian W. Damus <give.a.damus@gmail.com>
Diffstat (limited to 'plugins')
14 files changed, 861 insertions, 18 deletions
diff --git a/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram.ide.ui/plugin.xml b/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram.ide.ui/plugin.xml index fbae7965..b4e20078 100644 --- a/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram.ide.ui/plugin.xml +++ b/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram.ide.ui/plugin.xml @@ -13,6 +13,7 @@ Stefan Dirix - Bugs 456699, 474723 Camille Letavernier - Bug 515373 Christian W. Damus - bug 512529 + Simon Delisle - bug 217638 --> <plugin> @@ -36,6 +37,9 @@ <resourceSetHook class="org.eclipse.papyrus.compare.diagram.ide.ui.internal.ServicesRegistryInitializingHook"> </resourceSetHook> + <resourceSetHook + class="org.eclipse.papyrus.compare.diagram.ide.ui.internal.DiagramMigrationHook"> + </resourceSetHook> </extension> <extension point="org.eclipse.emf.compare.ide.ui.logicalModelViewHandlers"> diff --git a/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram.ide.ui/src/org/eclipse/papyrus/compare/diagram/ide/ui/internal/DiagramMigrationHook.java b/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram.ide.ui/src/org/eclipse/papyrus/compare/diagram/ide/ui/internal/DiagramMigrationHook.java new file mode 100644 index 00000000..551401e2 --- /dev/null +++ b/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram.ide.ui/src/org/eclipse/papyrus/compare/diagram/ide/ui/internal/DiagramMigrationHook.java @@ -0,0 +1,334 @@ +/******************************************************************************* + * Copyright (c) 2016, 2017 Ericsson and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Simon Delisle - initial API and implementation + * Christian W. Damus - bug 527638 + *******************************************************************************/ +package org.eclipse.papyrus.compare.diagram.ide.ui.internal; + +import static java.lang.String.format; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.emf.common.util.EList; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.util.EContentAdapter; +import org.eclipse.emf.ecore.util.InternalEList; +import org.eclipse.emf.ecore.xmi.XMLResource; +import org.eclipse.emf.transaction.TransactionalEditingDomain; +import org.eclipse.gmf.runtime.common.core.command.CompositeCommand; +import org.eclipse.gmf.runtime.common.core.command.ICommand; +import org.eclipse.gmf.runtime.notation.Diagram; +import org.eclipse.gmf.runtime.notation.NotationPackage; +import org.eclipse.papyrus.infra.emf.gmf.util.GMFUnsafe; +import org.eclipse.papyrus.infra.gmfdiag.common.model.NotationModel; +import org.eclipse.papyrus.infra.gmfdiag.common.reconciler.DiagramReconciler; +import org.eclipse.papyrus.infra.gmfdiag.common.reconciler.DiagramReconcilersReader; +import org.eclipse.papyrus.infra.gmfdiag.common.reconciler.DiagramVersioningUtils; + +/** + * Hook in the EMF Compare {@link ResourceSet} in order to make it able to handle papyrus diagram migration + * features. + * + * @author <a href="mailto:simon.delisle@ericsson.com">Simon Delisle</a> + */ +public class DiagramMigrationHook extends AbstractPapyrusResourceSetHook { + + @Override + public void preLoadingHook(ResourceSet resourceSet, Collection<? extends URI> uris) { + // Pass + } + + @Override + public void postLoadingHook(ResourceSet resourceSet, Collection<? extends URI> uris) { + for (Resource resource : resourceSet.getResources()) { + if (NotationModel.NOTATION_FILE_EXTENSION.equals(resource.getURI().fileExtension())) { + EList<EObject> contents = resource.getContents(); + for (EObject object : contents) { + if (object instanceof Diagram) { + Diagram diagram = (Diagram)object; + + IDGenerator idgen = new IDGenerator(diagram); + try { + migrateDiagram(getEditingDomain(resourceSet), diagram); + } finally { + idgen.dispose(); + } + } + } + } + } + } + + /** + * Get the editing domain for this resourceSet or create it. + * + * @param resourceSet + * The resource set + * @return TransactionalEditingDomain + */ + private TransactionalEditingDomain getEditingDomain(ResourceSet resourceSet) { + TransactionalEditingDomain existingDomain = TransactionalEditingDomain.Factory.INSTANCE + .getEditingDomain(resourceSet); + if (existingDomain == null) { + return TransactionalEditingDomain.Factory.INSTANCE.createEditingDomain(resourceSet); + } else { + return existingDomain; + } + } + + /** + * Build the migration command. + * + * @param diagram + * Diagram you want to migrate + * @return CompositeCommand + */ + private CompositeCommand buildCommand(Diagram diagram) { + CompositeCommand reconcileCommand = null; + + if (!DiagramVersioningUtils.isOfCurrentPapyrusVersion(diagram)) { + reconcileCommand = new CompositeCommand("Reconciling"); //$NON-NLS-1$ + + String sourceVersion = DiagramVersioningUtils.getCompatibilityVersion(diagram); + Map<String, Collection<DiagramReconciler>> diagramReconcilers = DiagramReconcilersReader + .getInstance().load(); + String diagramType = diagram.getType(); + Collection<DiagramReconciler> reconcilers = new LinkedList<DiagramReconciler>(); + if (diagramReconcilers.containsKey(diagramType)) { + reconcilers.addAll(diagramReconcilers.get(diagramType)); + } + + boolean someFailed = false; + Iterator<DiagramReconciler> reconciler = reconcilers.iterator(); + while (reconciler.hasNext() && !someFailed) { + DiagramReconciler next = reconciler.next(); + + if (!next.canReconcileFrom(diagram, sourceVersion)) { + // asked for ignore it for this instance, all fine + continue; + } + ICommand nextCommand = next.getReconcileCommand(diagram); + if (nextCommand == null) { + // legitimate no-op response, all fine + continue; + } + + if (nextCommand.canExecute()) { + reconcileCommand.add(nextCommand); + } else { + CompareDiagramIDEUIPapyrusPlugin.getDefault().getLog().log(new Status(IStatus.ERROR, + CompareDiagramIDEUIPapyrusPlugin.PLUGIN_ID, + "Diagram reconciler " + next + " failed to reconcile diagram: " + diagram)); //$NON-NLS-1$ //$NON-NLS-2$ + someFailed = true; + } + } + if (someFailed) { + reconcileCommand = null; + } + } + return reconcileCommand; + } + + /** + * Migrate the diagram to the current version of Papyrus. + * + * @param domain + * The editing domain for this diagram. + * @param diagram + * The diagram. + */ + private void migrateDiagram(TransactionalEditingDomain domain, Diagram diagram) { + CompositeCommand migrationCommand = buildCommand(diagram); + if (migrationCommand == null) { + domain.dispose(); + return; + } + + migrationCommand.add(DiagramVersioningUtils.createStampCurrentVersionCommand(diagram)); + // TODO: Handle reconcile exception + try { + // The migration command requires a transactional editing domain, but we don't + // want to record anything for undo/redo etc. + GMFUnsafe.write(domain, migrationCommand); + } catch (Exception e) { + logException(e); + } finally { + domain.dispose(); + } + } + + /** + * Logs the specified exception. + * + * @param t + * The exception to be logged. + */ + private static void logException(Throwable t) { + CompareDiagramIDEUIPapyrusPlugin.getDefault().getLog() + .log(new Status(IStatus.WARNING, CompareDiagramIDEUIPapyrusPlugin.PLUGIN_ID, + "Could not migrate the diagram before comparison", t)); //$NON-NLS-1$ + } + + // + // Nested types + // + + /** + * An adapter that generates new unique IDs, using a stable algorithm, for all new objects created by + * diagram migration. This helps to ensure that when the same diagram on multiple sides of a comparison is + * migrated on each side, that new elements created by migration are matched to avoid spurious add-add + * conflicts. + * + * @author Christian W. Damus + */ + protected static class IDGenerator extends EContentAdapter { + private static final Pattern NOT_NCNAME = Pattern.compile("[^-a-zA-Z0-9_.]"); //$NON-NLS-1$ + + private static final String CHILDREN_PAT = "%s.%d"; //$NON-NLS-1$ + + private static final String STYLES_PAT = "%s._%s"; //$NON-NLS-1$ + + private static final String OTHER_PAT = "%s.%s%d"; //$NON-NLS-1$ + + private final Matcher notNCName = NOT_NCNAME.matcher(""); //$NON-NLS-1$ + + private final EObject root; + + // Papyrus diagrams do not support cross-resource containment + private XMLResource resource; + + private boolean enabled; + + /** + * Initializes me for generation of predictable, matchable unique identifiers in the tree under the + * given {@code root}. + * + * @param root + * the root of a tree in which to generate unique identifiers + */ + public IDGenerator(EObject root) { + super(); + + this.root = root; + this.resource = (XMLResource)root.eResource(); + + root.eAdapters().add(this); + this.enabled = true; + } + + public void dispose() { + this.enabled = false; + root.eAdapters().remove(this); + } + + @Override + protected void setTarget(EObject target) { + if (enabled) { + generateID(target); + } + + super.setTarget(target); + } + + /** + * Generate a new unique ID for an {@code object} based on its containment in its parent object. + * + * @param object + * a new object for which to generate a predictable, matchable ID + */ + protected void generateID(EObject object) { + // Everything has a container because we can never attempt to generate + // the ID of the diagram, itself + EObject parent = object.eContainer(); + String parentID = resource.getID(parent); + String newID; + + EReference containment = object.eContainmentFeature(); + int index = indexOf(parent, containment, object); + if (containment == NotationPackage.Literals.VIEW__PERSISTED_CHILDREN) { + // For the most common case, the most compact scheme + newID = format(CHILDREN_PAT, parentID, Integer.valueOf(index)); + } else if (containment == NotationPackage.Literals.VIEW__STYLES) { + // Identify styles by EClass to avoid ordering issues + newID = format(STYLES_PAT, parentID, asID(object.eClass().getName())); + } else { + newID = format(OTHER_PAT, parentID, asID(containment.getName()), Integer.valueOf(index)); + } + + resource.setID(object, unique(newID)); + } + + /** + * Compures the index of an object {@code contained} in some {@code containment} reference of a + * {@code container}. + * + * @param container + * the containing object + * @param containment + * the reference in which the {@code contained} object is stored + * @param contained + * the contained object + * @return its index + */ + protected int indexOf(EObject container, EReference containment, EObject contained) { + if (containment.isMany()) { + return ((InternalEList<?>)container.eGet(containment, false)).basicIndexOf(contained); + } else { + return 0; + } + } + + /** + * Transforms a {@link name} from the Ecore model by removing characters that are not valid in an XMI + * ID. + * + * @param name + * an Ecore name + * @return a munged variant that is suitable for concatenation in an XMI ID (usually unchanged) + */ + protected String asID(String name) { + notNCName.reset(name); + return notNCName.replaceAll(""); //$NON-NLS-1$ + } + + /** + * Munge an object identifier, if necessary, to make it unique in the resource. + * + * @param id + * an object identifier + * @return an unique variant of the identifier (usually unchanged) + */ + protected String unique(String id) { + String base = id; + String result = base; + int suffix = -1; + + while (resource.getEObject(result) != null) { + if (suffix < 0) { + base = base + '_'; + } + suffix++; + result = base + suffix; + } + + return result; + } + } +} diff --git a/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram/META-INF/MANIFEST.MF b/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram/META-INF/MANIFEST.MF index f1e12348..035eca27 100644 --- a/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram/META-INF/MANIFEST.MF +++ b/plugins/compare/bundles/org.eclipse.papyrus.compare.diagram/META-INF/MANIFEST.MF @@ -9,7 +9,8 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.10.0", org.eclipse.emf.compare;bundle-version="3.2.0", org.eclipse.uml2.uml;bundle-version="5.0.0", org.eclipse.gmf.runtime.notation;bundle-version="1.8.0", - org.eclipse.papyrus.infra.core;bundle-version="0.9.1" + org.eclipse.papyrus.infra.core;bundle-version="0.9.1", + org.eclipse.papyrus.infra.gmfdiag.style;bundle-version="[1.0.0,2.0.0)" Bundle-ActivationPolicy: lazy Import-Package: com.google.common.base;version="[15.0.0,22.0.0)", com.google.common.collect;version="[15.0.0,22.0.0)" diff --git a/plugins/compare/pom.xml b/plugins/compare/pom.xml index 80ca27c0..e861274a 100755 --- a/plugins/compare/pom.xml +++ b/plugins/compare/pom.xml @@ -236,8 +236,11 @@ <ws>gtk</ws> <arch>x86_64</arch> </environment> - <!-- <environment> <os>macosx</os> <ws>cocoa</ws> <arch>x86_64</arch> - </environment> --> + <environment> + <os>macosx</os> + <ws>cocoa</ws> + <arch>x86_64</arch> + </environment> </environments> <target> <artifact> diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests.git/pom.xml b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests.git/pom.xml index 7ea91380..f8761c6f 100644 --- a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests.git/pom.xml +++ b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests.git/pom.xml @@ -26,7 +26,7 @@ <include>org/eclipse/papyrus/compare/diagram/tests/suite/PapyrusGitTests.class</include> </includes> <useUIHarness>true</useUIHarness> - <argLine>-Xmx2048m</argLine> + <argLine>${test-vmargs} -Xmx2048m</argLine> </configuration> </plugin> </plugins> diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/META-INF/MANIFEST.MF b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/META-INF/MANIFEST.MF index 59cdde65..d0e4f48b 100644 --- a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/META-INF/MANIFEST.MF +++ b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/META-INF/MANIFEST.MF @@ -41,7 +41,8 @@ Require-Bundle: org.eclipse.core.runtime, org.eclipse.emf.compare.uml2.ide.tests, org.eclipse.emf.compare.ide.ui.tests.framework, org.eclipse.papyrus.sysml14;bundle-version="1.0.0", - org.eclipse.papyrus.sysml14.architecture;bundle-version="1.0.0" + org.eclipse.papyrus.sysml14.architecture;bundle-version="1.0.0", + org.eclipse.papyrus.uml.diagram.component;bundle-version="3.0.0" Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Bundle-Vendor: %providerName Import-Package: com.google.common.base;version="[15.0.0,22.0.0)", diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/DiagramMigrationHookIntegrationTest.java b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/DiagramMigrationHookIntegrationTest.java new file mode 100644 index 00000000..1e43cd95 --- /dev/null +++ b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/DiagramMigrationHookIntegrationTest.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * Copyright (c) 2017 Christian W. Damus and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Christian W. Damus - initial API and implementation + *******************************************************************************/ +package org.eclipse.papyrus.compare.diagram.tests.migration; + +import static org.eclipse.papyrus.compare.diagram.tests.migration.DiagramMigrationHookTest.collectAll; +import static org.eclipse.papyrus.compare.diagram.tests.migration.DiagramMigrationHookTest.getTestInput; +import static org.eclipse.papyrus.compare.diagram.tests.migration.DiagramMigrationHookTest.hasVisualID; +import static org.eclipse.papyrus.compare.diagram.tests.migration.DiagramMigrationHookTest.isCurrentVersion; +import static org.hamcrest.CoreMatchers.any; +import static org.hamcrest.CoreMatchers.everyItem; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.compare.Comparison; +import org.eclipse.emf.compare.Conflict; +import org.eclipse.emf.compare.EMFCompare; +import org.eclipse.emf.compare.ide.ui.internal.logical.ComparisonScopeBuilder; +import org.eclipse.emf.compare.ide.ui.logical.SynchronizationModel; +import org.eclipse.emf.compare.ide.ui.tests.workspace.TestProject; +import org.eclipse.emf.compare.ide.utils.StorageTraversal; +import org.eclipse.emf.compare.rcp.internal.extension.impl.EMFCompareBuilderConfigurator; +import org.eclipse.emf.compare.scope.IComparisonScope; +import org.eclipse.emf.compare.uml2.ide.tests.util.ProfileTestUtil; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.gmf.runtime.notation.Diagram; +import org.eclipse.gmf.runtime.notation.View; +import org.eclipse.papyrus.compare.diagram.ide.ui.internal.DiagramMigrationHook; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for the {@link DiagramMigrationHook} class. + * + * @author Christian W. Damus + */ +@SuppressWarnings({"restriction", "nls" }) +public class DiagramMigrationHookIntegrationTest { + + static final List<String> FILE_NAMES = ImmutableList.of("model.di", "model.uml", "model.notation"); + + private TestProject project; + + private Map<String, IFile> files; + + /** + * Initializes me. + */ + public DiagramMigrationHookIntegrationTest() { + super(); + } + + @Test + public void hook() { + IComparisonScope scope = getComparisonScope(false); + + // Views no longer have numeric "visual ID" types + Iterable<View> views = collectAll((ResourceSet)scope.getLeft(), View.class); + assumeThat(views, hasItem(any(View.class))); + assertThat(views, everyItem(not(hasVisualID()))); + + // Diagrams are up-to-date + Iterable<Diagram> diagrams = Iterables.filter(views, Diagram.class); + assumeThat(diagrams, hasItem(any(Diagram.class))); + assertThat(diagrams, everyItem(isCurrentVersion())); + } + + @Test + public void noConflicts() { + IComparisonScope scope = getComparisonScope(true); + Comparison comparison = compare(scope); + + assertThat("Migration introduced different objects on each side that are in conflict", + comparison.getConflicts(), not(hasItem(any(Conflict.class)))); + } + + // + // Test framework + // + + @Before + public void createProject() throws Exception { + files = new LinkedHashMap<>(); + project = new TestProject(); + + for (String next : FILE_NAMES) { + files.put(next, project.createFile(next, getTestInput(next))); + } + } + + @After + public void deleteProject() throws CoreException, IOException { + files = null; + project.dispose(); + } + + IComparisonScope getComparisonScope(boolean threeWay) { + List<String> uris = new ArrayList<>(3); + for (IFile next : files.values()) { + uris.add(URI.createPlatformResourceURI(next.getFullPath().toString(), true).toString()); + } + + // Let the comparison scope machinery load the resources with all available hooks + final StorageTraversal traversal = ProfileTestUtil + .createStorageTraversal(uris.toArray(new String[uris.size()])); + + // Use the same resources for all sides (if we compare, it's to find spurious conflicts) + StorageTraversal origin = threeWay ? traversal : null; + final SynchronizationModel syncModel = new SynchronizationModel(traversal, traversal, origin); + + return ComparisonScopeBuilder.create(syncModel, new NullProgressMonitor()); + } + + Comparison compare(IComparisonScope scope) { + EMFCompare.Builder builder = EMFCompare.builder(); + EMFCompareBuilderConfigurator.createDefault().configure(builder); + return builder.build().compare(scope); + } +} diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/DiagramMigrationHookTest.java b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/DiagramMigrationHookTest.java new file mode 100644 index 00000000..98bb307d --- /dev/null +++ b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/DiagramMigrationHookTest.java @@ -0,0 +1,229 @@ +/******************************************************************************* + * Copyright (c) 2017 Christian W. Damus and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Christian W. Damus - initial API and implementation + *******************************************************************************/ +package org.eclipse.papyrus.compare.diagram.tests.migration; + +import static org.hamcrest.CoreMatchers.any; +import static org.hamcrest.CoreMatchers.everyItem; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeThat; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; +import com.google.common.collect.Lists; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.gmf.runtime.notation.Diagram; +import org.eclipse.gmf.runtime.notation.View; +import org.eclipse.papyrus.compare.diagram.ide.ui.internal.DiagramMigrationHook; +import org.eclipse.papyrus.infra.gmfdiag.common.reconciler.DiagramVersioningUtils; +import org.hamcrest.CustomTypeSafeMatcher; +import org.hamcrest.FeatureMatcher; +import org.hamcrest.Matcher; +import org.junit.After; +import org.junit.Test; + +/** + * Unit tests for the {@link DiagramMigrationHook} class. + */ +@SuppressWarnings("nls") +public class DiagramMigrationHookTest { + + private final DiagramMigrationHook fixture = new DiagramMigrationHook(); + + private final ResourceSet resourceSet = new ResourceSetImpl(); + + /** + * Initializes me. + */ + public DiagramMigrationHookTest() { + super(); + } + + @Test + public void postLoadingHook() { + Collection<URI> uris = uris("model.di", "model.uml", "model.notation"); + load(resourceSet, uris); + fixture.postLoadingHook(resourceSet, uris); + + // Views no longer have numeric "visual ID" types + Iterable<View> views = collectAll(resourceSet, View.class); + assumeThat(views, hasItem(any(View.class))); + assertThat(views, everyItem(not(hasVisualID()))); + + // Diagrams are up-to-date + Iterable<Diagram> diagrams = Iterables.filter(views, Diagram.class); + assumeThat(diagrams, hasItem(any(Diagram.class))); + assertThat(diagrams, everyItem(isCurrentVersion())); + } + + // + // Test framework + // + + /** + * Ensure that the UML {@code CacheAdapter} doesn't hold on to our resources. + */ + @After + public void unloadResourceSet() { + for (Resource next : resourceSet.getResources()) { + next.unload(); + next.eAdapters().clear(); + } + + resourceSet.getResources().clear(); + resourceSet.eAdapters().clear(); + } + + /** + * Create URIs from a bunch of strings. + * + * @param uri + * URI strings + * @return the corresponding URIs + */ + static List<URI> uris(String... uri) { + List<URI> result = new ArrayList<>(uri.length); + for (String next : uri) { + result.add(URI.createURI(next)); + } + return result; + } + + /** + * Obtains the contents of the test resource that is intended to be persisted at the given target URI. + * + * @param targetURI + * the URI of the resource as it would be persisted. The last segment of this URI is taken as + * the name to {@linkplain #getTestInput(String) get} + * @return the contents of the resource from the host bundle + * @throws IOException + * on failure to find/access the test resource + * @see #getTestInput(String) + */ + static InputStream getTestInput(URI targetURI) throws IOException { + return getTestInput(targetURI.lastSegment()); + } + + /** + * Obtains the contents of the {@code name}d test resource. + * + * @param name + * the test resource name to get + * @return the contents of the resource from the host bundle + * @throws IOException + * on failure to find/access the test resource + */ + static InputStream getTestInput(String name) throws IOException { + URL url = DiagramMigrationHookTest.class.getResource(String.format("data/a1/%s", name)); + return url.openStream(); + } + + /** + * Loads the given resource URIs in a resource set from the corresponding {@linkplain #getTestInput(URI) + * test resources} in the host bundle. + * + * @param rset + * the resource set context in which to load the resources + * @param uris + * the resource URIs to load + * @return the loaded resources + */ + static Collection<Resource> load(ResourceSet rset, Collection<URI> uris) { + List<Resource> result = new ArrayList<>(uris.size()); + + for (URI next : uris) { + Resource resource = rset.createResource(next); + try (InputStream input = getTestInput(next)) { + resource.load(input, null); + } catch (Exception e) { + e.printStackTrace(); + fail("Failed to load test resource: " + e.getMessage()); + } + result.add(resource); + } + + return result; + } + + /** + * Collect all objects of some {@code type} in a resource set. + * + * @param rset + * a resource set + * @param type + * the type of objects to retrieve + * @return the objects of the given {@code type} + */ + static <T> Iterable<T> collectAll(ResourceSet rset, Class<T> type) { + return Lists.newArrayList(Iterators.filter(rset.getAllContents(), type)); + } + + /** + * Hamcrest matcher for diagram views that have "visual ID" types. + * + * @return matches views that have numeric types + */ + static Matcher<View> hasVisualID() { + return new FeatureMatcher<View, String>(matchesRegex("\\d+"), "is an integer", "type") { + @Override + protected String featureValueOf(View actual) { + return actual.getType(); + } + }; + } + + /** + * Hamcrest matcher for regular expressions. + * + * @param regex + * a regular expression + * @return a matcher for strings matching the {@code regex} in their entirety + */ + static Matcher<String> matchesRegex(String regex) { + final java.util.regex.Matcher matcher = Pattern.compile(regex).matcher(""); + return new CustomTypeSafeMatcher<String>(String.format("matches regex '%s'", regex)) { + @Override + protected boolean matchesSafely(String item) { + matcher.reset(item); + return matcher.matches(); + } + }; + } + + /** + * Hamcrest matcher for diagrams that are at the current version. + * + * @return matches diagrams that are up-to-date + */ + static Matcher<Diagram> isCurrentVersion() { + return new CustomTypeSafeMatcher<Diagram>("is up-to-date") { + @Override + protected boolean matchesSafely(Diagram item) { + return DiagramVersioningUtils.isOfCurrentPapyrusVersion(item); + } + }; + } +} diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/data/a1/model.di b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/data/a1/model.di new file mode 100644 index 00000000..bf9abab3 --- /dev/null +++ b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/data/a1/model.di @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI"/> diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/data/a1/model.notation b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/data/a1/model.notation new file mode 100644 index 00000000..49a6e24f --- /dev/null +++ b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/data/a1/model.notation @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<notation:Diagram xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:notation="http://www.eclipse.org/gmf/runtime/1.0.2/notation" xmlns:style="http://www.eclipse.org/papyrus/infra/viewpoints/policy/style" xmlns:uml="http://www.eclipse.org/uml2/5.0.0/UML" xmi:id="_tvKQsMKAEeS3-futUG4ugQ" type="PapyrusUMLComponentDiagram" name="ComponentDiagram" measurementUnit="Pixel"> + <children xmi:type="notation:Shape" xmi:id="_1V700MKAEeS3-futUG4ugQ" type="2002"> + <children xmi:type="notation:DecorationNode" xmi:id="_1WICEMKAEeS3-futUG4ugQ" type="5004"/> + <children xmi:type="notation:DecorationNode" xmi:id="_1WIpIMKAEeS3-futUG4ugQ" type="6030"> + <layoutConstraint xmi:type="notation:Location" xmi:id="_1WIpIcKAEeS3-futUG4ugQ" y="5"/> + </children> + <children xmi:type="notation:BasicCompartment" xmi:id="_1WM6kMKAEeS3-futUG4ugQ" type="7001"> + <styles xmi:type="notation:TitleStyle" xmi:id="_1WM6kcKAEeS3-futUG4ugQ"/> + <layoutConstraint xmi:type="notation:Bounds" xmi:id="_1WM6ksKAEeS3-futUG4ugQ"/> + </children> + <element xmi:type="uml:Component" href="model.uml#_1GdOYMKAEeS3-futUG4ugQ"/> + <layoutConstraint xmi:type="notation:Bounds" xmi:id="_1V700cKAEeS3-futUG4ugQ" x="59" y="55" width="116"/> + </children> + <children xmi:type="notation:Shape" xmi:id="_QL7KYMKCEeS3-futUG4ugQ" type="2002"> + <children xmi:type="notation:DecorationNode" xmi:id="_QL7xcMKCEeS3-futUG4ugQ" type="5004"/> + <children xmi:type="notation:DecorationNode" xmi:id="_QL8YgMKCEeS3-futUG4ugQ" type="6030"> + <layoutConstraint xmi:type="notation:Location" xmi:id="_QL8YgcKCEeS3-futUG4ugQ" y="5"/> + </children> + <children xmi:type="notation:BasicCompartment" xmi:id="_QL8YgsKCEeS3-futUG4ugQ" type="7001"> + <styles xmi:type="notation:TitleStyle" xmi:id="_QL8Yg8KCEeS3-futUG4ugQ"/> + <layoutConstraint xmi:type="notation:Bounds" xmi:id="_QL8YhMKCEeS3-futUG4ugQ"/> + </children> + <element xmi:type="uml:Component" href="model.uml#_QLtH8MKCEeS3-futUG4ugQ"/> + <layoutConstraint xmi:type="notation:Bounds" xmi:id="_QL7KYcKCEeS3-futUG4ugQ" x="208" y="57" width="116"/> + </children> + <styles xmi:type="notation:StringValueStyle" xmi:id="_tvKQscKAEeS3-futUG4ugQ" name="diagram_compatibility_version" stringValue="1.0.0"/> + <styles xmi:type="notation:DiagramStyle" xmi:id="_tvKQssKAEeS3-futUG4ugQ"/> + <styles xmi:type="style:PapyrusViewStyle" xmi:id="_tvKQs8KAEeS3-futUG4ugQ"> + <owner xmi:type="uml:Model" href="model.uml#_tsn6EMKAEeS3-futUG4ugQ"/> + </styles> + <element xmi:type="uml:Model" href="model.uml#_tsn6EMKAEeS3-futUG4ugQ"/> +</notation:Diagram> diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/data/a1/model.uml b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/data/a1/model.uml new file mode 100644 index 00000000..8d76f14f --- /dev/null +++ b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/migration/data/a1/model.uml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<uml:Model xmi:version="20131001" xmlns:xmi="http://www.omg.org/spec/XMI/20131001" xmlns:uml="http://www.eclipse.org/uml2/5.0.0/UML" xmi:id="_tsn6EMKAEeS3-futUG4ugQ" name="Model"> + <packageImport xmi:type="uml:PackageImport" xmi:id="_tsn6EcKAEeS3-futUG4ugQ"> + <importedPackage xmi:type="uml:Model" href="pathmap://UML_LIBRARIES/UMLPrimitiveTypes.library.uml#_0"/> + </packageImport> + <packagedElement xmi:type="uml:Component" xmi:id="_1GdOYMKAEeS3-futUG4ugQ" name="C1"/> + <packagedElement xmi:type="uml:Component" xmi:id="_QLtH8MKCEeS3-futUG4ugQ" name="C2"/> +</uml:Model> diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/saveparameter/SaveParameterHookIntegrationTest.java b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/saveparameter/SaveParameterHookIntegrationTest.java index 7d46e36e..d5a180ed 100644 --- a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/saveparameter/SaveParameterHookIntegrationTest.java +++ b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/saveparameter/SaveParameterHookIntegrationTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 EclipseSource Muenchen GmbH and others. + * Copyright (c) 2015, 2017 EclipseSource Muenchen GmbH and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,10 +7,13 @@ * * Contributors: * Stefan Dirix - initial API and implementation + * Christian W. Damus - bug 527638 *******************************************************************************/ package org.eclipse.papyrus.compare.diagram.tests.saveparameter; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; import com.google.common.base.Charsets; import com.google.common.io.Files; @@ -22,12 +25,15 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.compare.ide.ui.internal.logical.ComparisonScopeBuilder; @@ -40,6 +46,9 @@ import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.resource.URIConverter; import org.eclipse.emf.ecore.resource.impl.ExtensibleURIConverterImpl; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.emf.ecore.xmi.XMLResource; +import org.eclipse.papyrus.compare.diagram.ide.ui.internal.DiagramMigrationHook; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -51,7 +60,7 @@ import org.osgi.framework.Bundle; * * @author Stefan Dirix <sdirix@eclipsesource.com> */ -@SuppressWarnings("restriction") +@SuppressWarnings({"restriction", "nls" }) public class SaveParameterHookIntegrationTest { /** @@ -120,22 +129,39 @@ public class SaveParameterHookIntegrationTest { final List<IFile> originalFiles = new ArrayList<IFile>(fileNames.size()); final List<IFile> saveFiles = new ArrayList<IFile>(fileNames.size()); - final List<String> platformURIStrings = new ArrayList<String>(fileNames.size()); + final List<URI> originalResourceURIs = new ArrayList<>(fileNames.size()); + final List<String> saveResourceURIStrings = new ArrayList<>(fileNames.size()); + final Map<IFile, IFile> originalToSaveMap = new HashMap<>(); - // copy files twice + // Create originals for (String fileName : fileNames) { final String path = BASE_PATH + localContainer + fileName; final URI fileURI = getFileUri(bundle.getEntry(path)); final IFile originalFile = project.createFile("original/" + fileName, getInputStream(fileURI)); - final IFile saveFile = project.createFile("save/" + fileName, getInputStream(fileURI)); + final IFile saveFile = project.getProject().getFile(new Path("save/" + fileName)); + originalToSaveMap.put(originalFile, saveFile); + originalFiles.add(originalFile); + originalResourceURIs + .add(URI.createPlatformResourceURI(originalFile.getFullPath().toString(), true)); + } + + // Run diagram migrations as needed, to avoid those introducing changes + migrateDiagrams(originalResourceURIs); + + // Copy the migrated files. Be careful to maintain ordering + project.createFolder("save"); + for (IFile originalFile : originalFiles) { + IFile saveFile = originalToSaveMap.get(originalFile); + saveFile.create(originalFile.getContents(true), true, null); saveFiles.add(saveFile); - platformURIStrings.add("platform:/resource/" + TEST_PROJECT_NAME + "/save/" + fileName); + saveResourceURIStrings + .add(URI.createPlatformResourceURI(saveFile.getFullPath().toString(), true).toString()); } // build comparison scope which uses the internal resource sets of EMF Compare final StorageTraversal traversal = ProfileTestUtil - .createStorageTraversal(platformURIStrings.toArray(new String[0])); + .createStorageTraversal(saveResourceURIStrings.toArray(new String[0])); final SynchronizationModel syncModel = new SynchronizationModel(traversal, traversal, null); final IComparisonScope scope = ComparisonScopeBuilder.create(syncModel, new NullProgressMonitor()); @@ -149,7 +175,7 @@ public class SaveParameterHookIntegrationTest { for (int i = 0; i < fileNames.size(); i++) { final IFile originalFile = originalFiles.get(i); final IFile saveFile = saveFiles.get(i); - assertTrue(compareTextContent(originalFile, saveFile)); + compareTextContent(originalFile, saveFile, true); } } @@ -165,11 +191,15 @@ public class SaveParameterHookIntegrationTest { * @throws IOException * When there is an error reading one of the files. */ - private boolean compareTextContent(IFile fileA, IFile fileB) throws IOException { + private void compareTextContent(IFile fileA, IFile fileB, boolean expectedEqual) throws IOException { final String textFileA = removeCRCharacters(getText(fileA)); final String textFileB = removeCRCharacters(getText(fileB)); - return textFileA.equals(textFileB); + if (expectedEqual) { + assertEquals(textFileB, textFileA); + } else { + assertNotEquals(textFileB, textFileA); + } } /** @@ -248,4 +278,40 @@ public class SaveParameterHookIntegrationTest { public void disposeProject() throws CoreException, IOException { project.dispose(); } + + /** + * Migrate the diagrams in a bunch of resources and save them. + * + * @param resourceURIs + * the resources in which to migrate diagrams + */ + void migrateDiagrams(Collection<URI> resourceURIs) { + ResourceSet rset = new ResourceSetImpl(); + + DiagramMigrationHook hook = new DiagramMigrationHook(); + for (URI next : resourceURIs) { + rset.getResource(next, true); + } + + hook.postLoadingHook(rset, resourceURIs); + + // Save the migrations. Note that the original test resources have the + // redundant XMI types, so preserve those + Map<Object, Object> saveOptions = new HashMap<>(); + saveOptions.put(XMLResource.OPTION_SAVE_TYPE_INFORMATION, Boolean.TRUE); + saveOptions.put(Resource.OPTION_SAVE_ONLY_IF_CHANGED, Boolean.TRUE); + for (Resource next : rset.getResources()) { + try { + next.save(saveOptions); + } catch (IOException e) { + e.printStackTrace(); + fail("Failed to save migrated diagram(s): " + e.getMessage()); + } + } + + for (Resource next : rset.getResources()) { + next.unload(); + } + rset.getResources().clear(); + } } diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/suite/AllTests.java b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/suite/AllTests.java index 73a628b6..d2789ce6 100644 --- a/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/suite/AllTests.java +++ b/plugins/compare/tests/org.eclipse.papyrus.compare.diagram.tests/src/org/eclipse/papyrus/compare/diagram/tests/suite/AllTests.java @@ -10,7 +10,7 @@ * Stefan Dirix - add ModelExtensionUtil, SaveParameterHook and URIAttachment tests * Philip Langer - add IngoreDiFileModelElementsTest * Martin Fleck - add MergeNonConflictingCascadingDifferencesFilterTest - * Christian W. Damus - bug 512529 + * Christian W. Damus - bugs 512529, 527638 *******************************************************************************/ package org.eclipse.papyrus.compare.diagram.tests.suite; @@ -29,6 +29,8 @@ import org.eclipse.papyrus.compare.diagram.tests.merge.AssocMergeTest; import org.eclipse.papyrus.compare.diagram.tests.merge.EdgeMergeTest; import org.eclipse.papyrus.compare.diagram.tests.merge.NodeMergeTest; import org.eclipse.papyrus.compare.diagram.tests.merge.sysml.MergeDiffInvolvingRefineDiffTest; +import org.eclipse.papyrus.compare.diagram.tests.migration.DiagramMigrationHookIntegrationTest; +import org.eclipse.papyrus.compare.diagram.tests.migration.DiagramMigrationHookTest; import org.eclipse.papyrus.compare.diagram.tests.modelextension.ModelExtensionUtilTest; import org.eclipse.papyrus.compare.diagram.tests.saveparameter.SaveParameterHookIntegrationTest; import org.eclipse.papyrus.compare.diagram.tests.saveparameter.SaveParameterHookTest; @@ -56,6 +58,7 @@ import org.junit.runners.Suite.SuiteClasses; IgnoreDiFilePostProcessorTest.class, PapyrusContextUtilTest.class, MergeNonConflictingCascadingFilterTest.class, MergeDiffInvolvingRefineDiffTest.class, // CSSTest.class, // + DiagramMigrationHookTest.class, DiagramMigrationHookIntegrationTest.class, // }) public class AllTests { diff --git a/plugins/compare/tests/pom.xml b/plugins/compare/tests/pom.xml index 51eb4354..9fc00c89 100644 --- a/plugins/compare/tests/pom.xml +++ b/plugins/compare/tests/pom.xml @@ -12,8 +12,24 @@ <properties> <target.file>${target.folder}/compare.tests-${target.stream}</target.file> + <test-vmargs></test-vmargs> </properties> + <profiles> + <profile> + <id>mac-test</id> + <activation> + <os> + <family>mac</family> + </os> + </activation> + <properties> + <target.file>${target.folder}/compare.tests-${target.stream}</target.file> + <test-vmargs>-XstartOnFirstThread</test-vmargs> + </properties> + </profile> + </profiles> + <build> <plugins> <plugin> @@ -21,10 +37,10 @@ <artifactId>tycho-surefire-plugin</artifactId> <configuration> <useUIHarness>true</useUIHarness> + <argLine>${test-vmargs}</argLine> </configuration> </plugin> </plugins> - </build> <modules> <module>org.eclipse.papyrus.compare.uml2.tests</module> |