blob: c8f1e2130981f02161ae39edd618b78183bc8c19 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017 EclipseSource Services 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Martin Fleck - initial API and implementation
*******************************************************************************/
package org.eclipse.papyrus.compare.diagram.tests.structuremergeviewer.actions;
import static com.google.common.collect.Iterables.all;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.isEmpty;
import static com.google.common.collect.Iterables.size;
import static org.eclipse.emf.compare.DifferenceState.MERGED;
import static org.eclipse.emf.compare.DifferenceState.UNRESOLVED;
import static org.eclipse.emf.compare.merge.AbstractMerger.SUB_DIFF_AWARE_OPTION;
import static org.eclipse.emf.compare.utils.EMFComparePredicates.containsConflictOfTypes;
import static org.eclipse.emf.compare.utils.EMFComparePredicates.hasState;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.Conflict;
import org.eclipse.emf.compare.ConflictKind;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.DifferenceState;
import org.eclipse.emf.compare.MatchResource;
import org.eclipse.emf.compare.ide.ui.internal.structuremergeviewer.actions.MergeNonConflictingRunnable;
import org.eclipse.emf.compare.ide.ui.tests.framework.RuntimeTestRunner;
import org.eclipse.emf.compare.ide.ui.tests.framework.annotations.Compare;
import org.eclipse.emf.compare.ide.ui.tests.framework.internal.CompareTestSupport;
import org.eclipse.emf.compare.internal.merge.MergeMode;
import org.eclipse.emf.compare.merge.IMergeOptionAware;
import org.eclipse.emf.compare.merge.IMerger.Registry;
import org.eclipse.emf.compare.rcp.EMFCompareRCPPlugin;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.uml2.uml.Class;
import org.eclipse.uml2.uml.Model;
import org.eclipse.uml2.uml.Package;
import org.eclipse.uml2.uml.PackageableElement;
import org.junit.runner.RunWith;
/**
* <p>
* This class tests the expected behavior of accepting non-conflicting changes when the cascading differences
* filter is enabled. The cascading differences filter handles the setting of sub-diff awareness in mergers.
* </p>
* <p>
* In particular, we test that bug 487151 is fixed where conflicting diffs are merged when 'accepting
* non-conflicting changes'. The source of this bug lies in the sub-diff awareness of mergers that merge
* sub-diffs, their dependendies, their sub-diffs, etc. Therefore, this bug only occurs when sub-diff
* awareness ('cascading differences filter' to the user) is enabled.
* </p>
* We use the following structure to test the correct behavior:
* <ul>
* <li>Origin: Package 'Package1' containing class 'Class1'</li>
* <li>Left: Package 'Package1' (class 'Class1' removed)</li>
* <li>Right: Package 'Package1' containing class 'Class1', and class 'Class2' subclassing class 'Class1'
* (class 'Class2' and generalization added)
* </ul>
* For EMF Compare this means that we have the following relationships between the UML differences:
* <ul>
* <li>L1: Left DELETE Class 1 (conflicts with R4)</li>
* <li>R1: Right ADD Class 2 (subDiffs because also related to Class 2: R2 and R3)</li>
* <li>R2: Right ADD Generalization (refinedBy R3 and R4)</li>
* <li>R3: Right ADD generalization to Class2.generalization</li>
* <li>R4: Right CHANGE generalization.general to Class 1 (conflicts with L1)</li>
* </ul>
* <p>
* In the comparison, we detect a conflict between adding the generalization, setting the 'general' of the
* generalization and the deletion of class 'Class1'. When accepting all non-conflicting changes (left changes
* accepted, right changes merged into left for non-conflicts), we expect the left model to add nothing when
* sub-diffs are enabled and to add Class 2 (R1) when sub-diffs are not enabled. The generalization (R2-R4)
* should not be added in any case since in UML we consider the addition of the generalization and the setting
* of the 'general' as a whole (DirectedRelationshipChange) and the 'general' can not be set since 'Class2'
* has been removed (conflict).
* </p>
* <p>
* All non-conflicting diffs should be in state {@link DifferenceState#MERGED} and all conflicting diffs
* should be in state {@link DifferenceState#UNRESOLVED}.
* </p>
*
* @author Martin Fleck <mfleck@eclipsesource.com>
* @see https://bugs.eclipse.org/bugs/show_bug.cgi?id=487151
*/
@SuppressWarnings({"restriction", "nls", "boxing" })
@RunWith(RuntimeTestRunner.class)
public class MergeNonConflictingCascadingFilterTest {
/** Cached cascading options before the last time the filter was enabled or disabled. */
private static final Map<IMergeOptionAware, Object> CACHED_OPTIONS = Maps.newHashMap();
private static Registry MERGER_REGISTRY = EMFCompareRCPPlugin.getDefault().getMergerRegistry();
/** Filter On setting **/
private static boolean FILTER_ON = true;
/** Filter Off setting **/
private static boolean FILTER_OFF = false;
/**
* <p>
* Tests that bug 487151 is fixed when the cascading differences filter is turned ON and graphical
* elements and their relationships are involved.
* </p>
*/
@Compare(left = "data/bug487151/classes/left/left.notation", right = "data/bug487151/classes/right/right.notation", ancestor = "data/bug487151/classes/origin/ancestor.notation")
public void testBug487151_Papyrus_On(final Comparison comparison) {
// lots of graphical diffs, details not important
Integer nrDiffs = null;
// 1 REAL structural and 1 REAL graphical
int nrRealConflicts = 2;
// graphical diffs are not conflicting
boolean hasNonConflictingDiffs = true;
// conflicting structural differences: LEFT delete Class1, RIGHT change Generalization.general, RIGHT
// add DirectedRelationshipChange + graphical differences: ...
boolean hasConflictingDiffs = true;
assertUMLDiffsAndStructure(comparison, FILTER_ON, nrDiffs, nrRealConflicts, hasNonConflictingDiffs,
hasConflictingDiffs);
}
/**
* <p>
* Tests that bug 487151 is fixed when the cascading differences filter is turned OFF and graphical
* elements and their relationships are involved.
* </p>
*/
@Compare(left = "data/bug487151/classes/left/left.notation", right = "data/bug487151/classes/right/right.notation", ancestor = "data/bug487151/classes/origin/ancestor.notation")
public void testBug487151_Papyrus_Off(final Comparison comparison) {
// lots of graphical diffs, details not important
Integer nrDiffs = null;
// 1 REAL structural and 1 REAL graphical
int nrRealConflicts = 2;
// graphical diffs are not conflicting
boolean hasNonConflictingDiffs = true;
// conflicting structural differences: LEFT delete Class1, RIGHT change Generalization.general, RIGHT
// add DirectedRelationshipChange + graphical differences: ...
boolean hasConflictingDiffs = true;
assertUMLDiffsAndStructure(comparison, FILTER_OFF, nrDiffs, nrRealConflicts, hasNonConflictingDiffs,
hasConflictingDiffs);
}
/**
* <p>
* Tests that bug 487151 is fixed when the cascading differences filter is turned ON and only UML elements
* and their relationships are involved.
* </p>
*/
@Compare(left = "data/bug487151/classes/left/left.uml", right = "data/bug487151/classes/right/right.uml", ancestor = "data/bug487151/classes/origin/ancestor.uml")
public void testBug487151_UML_On(final Comparison comparison) {
// 5 differences: LEFT delete Class1, RIGHT add Class2, RIGHT add generalization, RIGHT change
// Generalization.general, RIGHT add DirectedRelationshipChange (refining add/change for
// Generalization; from UML PostProcessor)
int nrDiffs = 5;
// 1 REAL structural conflict
int nrRealConflicts = 1;
// all diffs are in conflict through their relationships
boolean hasNonConflictingDiffs = false;
boolean hasConflictingDiffs = true;
assertUMLDiffsAndStructure(comparison, FILTER_ON, nrDiffs, nrRealConflicts, hasNonConflictingDiffs,
hasConflictingDiffs);
}
/**
* <p>
* Tests that bug 487151 is fixed when the cascading differences filter is turned OFF and only UML
* elements and their relationships are involved.
* </p>
*/
@Compare(left = "data/bug487151/classes/left/left.uml", right = "data/bug487151/classes/right/right.uml", ancestor = "data/bug487151/classes/origin/ancestor.uml")
public void testBug487151_UML_Off(final Comparison comparison) {
// 5 differences: LEFT delete Class1, RIGHT add Class2, RIGHT add generalization, RIGHT change
// Generalization.general, RIGHT add DirectedRelationshipChange (refining add/change for
// Generalization; from UML PostProcessor)
int nrDiffs = 5;
// 1 REAL structural conflict
int nrRealConflicts = 1;
// all diffs are in conflict through their relationships
boolean hasNonConflictingDiffs = true;
boolean hasConflictingDiffs = true;
assertUMLDiffsAndStructure(comparison, FILTER_OFF, nrDiffs, nrRealConflicts, hasNonConflictingDiffs,
hasConflictingDiffs);
}
/**
* <p>
* Tests that bug 487151 is fixed when the cascading differences filter is turned ON and and Ecore
* representation is used. Since we do not use refinement in Ecore and the Generalization is represented
* as a reference, Class 2 can be added independently of the filter setting.
* </p>
* <ul>
* <li>Origin: Package 'Package1' containing class 'Class1'</li>
* <li>Left: Package 'Package1' (class 'Class1' removed)</li>
* <li>Right: Package 'Package1' containing class 'Class1' and class 'Class2' with reference named
* 'Generalization' that has as eType 'Class1' (class 'Class2' and added generalization reference)
* </ul>
* <p>
* In the comparison, we detect a conflict between setting the eType of the 'Generalization' reference and
* the deletion of class 'Class1'. When accepting all non-conflicting changes (left changes accepted,
* right changes merged into left for non-conflicts), we expect the left model to have 'Class2' AND the
* 'Generalization' reference without an eType since 'Class1' has been deleted. All non-conflicting diffs
* should be in state {@link DifferenceState#MERGED} and all conflicting diffs should be in state
* {@link DifferenceState#UNRESOLVED}.
* </p>
*/
@Compare(left = "data/bug487151/ecore/left/left.ecore", right = "data/bug487151/ecore/right/right.ecore", ancestor = "data/bug487151/ecore/origin/ancestor.ecore")
public void testBug487151_Ecore_On(final Comparison comparison, final CompareTestSupport support) {
// 4 differences: LEFT delete Class1, RIGHT add Class2, RIGHT add generalization, RIGHT change
// Generalization.general
int nrDiffs = 4;
// 1 conflict (REAL)
int nrRealConflicts = 1;
// non-conflicting difference: RIGHT add Class2
boolean hasNonConflictingDiffs = true;
// conflicting differences: LEFT delete Class1, RIGHT change Generalization.eType, RIGHT add
// DirectedRelationshipChange
boolean hasConflictingDiffs = true;
assertEcoreDiffsAndStructure(comparison, support, FILTER_ON, nrDiffs, nrRealConflicts,
hasNonConflictingDiffs, hasConflictingDiffs);
}
/**
* <p>
* Tests that bug 487151 is fixed when the cascading differences filter is turned ON and and Ecore
* representation is used. Since we do not use refinement in Ecore and the Generalization is represented
* as a reference, Class 2 can be added independently of the filter setting.
* </p>
* <ul>
* <li>Origin: Package 'Package1' containing class 'Class1'</li>
* <li>Left: Package 'Package1' (class 'Class1' removed)</li>
* <li>Right: Package 'Package1' containing class 'Class1' and class 'Class2' with reference named
* 'Generalization' that has as eType 'Class1' (class 'Class2' and added generalization reference)
* </ul>
* <p>
* In the comparison, we detect a conflict between setting the eType of the 'Generalization' reference and
* the deletion of class 'Class1'. When accepting all non-conflicting changes (left changes accepted,
* right changes merged into left for non-conflicts), we expect the left model to have 'Class2' AND the
* 'Generalization' reference without an eType since 'Class1' has been deleted. All non-conflicting diffs
* should be in state {@link DifferenceState#MERGED} and all conflicting diffs should be in state
* {@link DifferenceState#UNRESOLVED}.
* </p>
*/
@Compare(left = "data/bug487151/ecore/left/left.ecore", right = "data/bug487151/ecore/right/right.ecore", ancestor = "data/bug487151/ecore/origin/ancestor.ecore")
public void testBug487151_Ecore_Off(final Comparison comparison, final CompareTestSupport support) {
// 4 differences: LEFT delete Class1, RIGHT add Class2, RIGHT add generalization, RIGHT change
// Generalization.general
int nrDiffs = 4;
// 1 conflict (REAL)
int nrRealConflicts = 1;
// non-conflicting difference: RIGHT add Class2
boolean hasNonConflictingDiffs = true;
// conflicting differences: LEFT delete Class1, RIGHT change Generalization.eType, RIGHT add
// DirectedRelationshipChange
boolean hasConflictingDiffs = true;
assertEcoreDiffsAndStructure(comparison, support, FILTER_OFF, nrDiffs, nrRealConflicts,
hasNonConflictingDiffs, hasConflictingDiffs);
}
/**
* Asserts the expected differences and structure for Ecore models. Specifically, that independently of
* the filter setting, the class 2 and its generalization is added, but the type of the generalization is
* not set.
*
* @param comparison
* comparison
* @param support
* test support for the comparison
* @param filter
* sub-diff filter setting
* @param nrDiffs
* expected number of differences
* @param nrRealConflicts
* expected number of real conflicts
* @param hasNonConflictingDiffs
* whether non-conflicting diffs are expected
* @param hasConflictingDiffs
* whether conflicting diffs are expected
*/
protected void assertEcoreDiffsAndStructure(Comparison comparison, CompareTestSupport support,
boolean filter, int nrDiffs, int nrRealConflicts, boolean hasNonConflictingDiffs,
boolean hasConflictingDiffs) {
final List<Diff> differences = comparison.getDifferences();
final List<Conflict> conflicts = comparison.getConflicts();
// 4 differences: LEFT delete Class1, RIGHT add Class2, RIGHT add generalization, RIGHT change
// Generalization.general
assertEquals(nrDiffs, differences.size());
// real conflicts
Iterable<Conflict> realConflicts = filter(conflicts, containsConflictOfTypes(ConflictKind.REAL));
assertEquals(nrRealConflicts, size(realConflicts));
// before accepting any changes, all differences are unresolved
assertTrue(all(differences, hasState(UNRESOLVED)));
// accept non-conflicting changes and collect differences
Iterable<Diff> nonConflictingDifferences = acceptNonConflictingChanges(comparison, filter);
assertEquals(hasNonConflictingDiffs, !isEmpty(nonConflictingDifferences));
// conflicting differences: LEFT delete Class1, RIGHT change Generalization.eType, RIGHT add
// DirectedRelationshipChange
final List<Diff> conflictingDifferences = getConflictingDifferences(
Lists.newArrayList(nonConflictingDifferences), conflicts);
assertEquals(hasConflictingDiffs, !isEmpty(conflictingDifferences));
// all accepted differences have been merged
assertTrue(all(nonConflictingDifferences, hasState(MERGED)));
// any differences part of real conflicts are still unresolved
assertTrue(all(conflictingDifferences, hasState(UNRESOLVED)));
// assert new structure (right changes merged into left for non-conflicts)
EObject root = support.getLeftResource().getContents().get(0);
assertNotNull(root);
assertTrue(root instanceof EPackage);
EPackage package1 = (EPackage)root;
assertNotNull(package1);
EClassifier class1 = package1.getEClassifier("Class1");
assertNull(class1); // still null due to conflict
EClassifier class2 = package1.getEClassifier("Class2");
// regardless of filter, class 2 and generalization is added
assertNotNull(class2); // newly added
EClass class2class = (EClass)class2;
EStructuralFeature class2generalization = class2class.getEStructuralFeature("Generalization");
assertNotNull(class2generalization); // reference generalization has been added
assertNull(class2generalization.getEType()); // no type set due to conflict
}
/**
* Asserts the expected differences and structure for UML models. Specifically, whether Class 2
* conflicting with sub-diff filter enabled is added.
*
* @param comparison
* comparison
* @param filter
* sub-diff filter setting
* @param nrDiffs
* expected number of differences
* @param nrRealConflicts
* expected number of real conflicts
* @param hasNonConflictingDiffs
* whether non-conflicting diffs are expected
* @param hasConflictingDiffs
* whether conflicting diffs are expected
*/
protected void assertUMLDiffsAndStructure(Comparison comparison, boolean filter, Integer nrDiffs,
int nrRealConflicts, boolean hasNonConflictingDiffs, boolean hasConflictingDiffs) {
final List<Diff> differences = comparison.getDifferences();
final List<Conflict> conflicts = comparison.getConflicts();
if (nrDiffs != null) {
assertEquals(nrDiffs.intValue(), differences.size());
}
Iterable<Conflict> realConflicts = filter(conflicts, containsConflictOfTypes(ConflictKind.REAL));
assertEquals(nrRealConflicts, size(realConflicts));
// before accepting any changes, all differences are unresolved
assertTrue(all(differences, hasState(UNRESOLVED)));
// accept non-conflicting changes and collect differences
Iterable<Diff> nonConflictingDifferences = acceptNonConflictingChanges(comparison, filter);
assertEquals(hasNonConflictingDiffs, !isEmpty(nonConflictingDifferences));
// get conflicting diffs
final List<Diff> conflictingDifferences = getConflictingDifferences(
Lists.newArrayList(nonConflictingDifferences), Lists.newArrayList(realConflicts));
assertEquals(hasConflictingDiffs, !conflictingDifferences.isEmpty());
// all accepted differences have been merged
assertTrue(all(nonConflictingDifferences, hasState(MERGED)));
// any differences part of real conflicts are still unresolved
assertTrue(all(conflictingDifferences, hasState(UNRESOLVED)));
// assert new structure (right changes merged into left for non-conflicts)
EObject root = getLeftUMLRoot(comparison);
assertNotNull(root);
assertTrue(root instanceof Model);
Model model = (Model)root;
Package package1 = (Package)model.getPackagedElement("Package1");
assertNotNull(package1);
PackageableElement class1 = package1.getPackagedElement("Class1");
assertNull(class1); // still null due to conflict
PackageableElement class2 = package1.getPackagedElement("Class2");
if (filter == FILTER_OFF) {
assertNotNull(class2);
Class class2class = (Class)class2;
assertTrue(class2class.getGeneralizations().isEmpty()); // no generalization added due to conflict
} else {
assertNull(class2); // class not added, because sub-diffs are in conflict
}
}
/**
* Returns the root element of the left UML resource or null if no such element can be found.
*
* @param comparison
* comparison
* @return left root element
*/
protected static EObject getLeftUMLRoot(Comparison comparison) {
for (MatchResource resource : comparison.getMatchedResources()) {
if (resource.getLeft().getURI().lastSegment().endsWith("uml")) {
return resource.getLeft().getContents().get(0);
}
}
return null;
}
/**
* Accepts all non-conflicting changes. The left changes will be accepted and the right changes will be
* merged into the left-hand side.
*
* @param comparison
* comparison with differences
* @param leftToRight
* direction of merge
* @return affected differences
*/
protected static Iterable<Diff> acceptNonConflictingChanges(Comparison comparison,
boolean cascadingFilter) {
boolean leftToRight = false;
boolean isLeftEditable = true;
boolean isRightEditable = false;
setCascadingFilter(cascadingFilter);
MergeNonConflictingRunnable mergeNonConflicting = new MergeNonConflictingRunnable(isLeftEditable,
isRightEditable, MergeMode.ACCEPT, null);
Iterable<Diff> mergedDiffs = mergeNonConflicting.merge(comparison, leftToRight, MERGER_REGISTRY);
restoreCascadingFilter();
return mergedDiffs;
}
/**
* Returns a list of all differences that part of a conflict and ensures that there is no overlap between
* conflicting and non-conflicting diffs.
*
* @param realConflicts
* conflicts
* @return conflicting differences
*/
protected static List<Diff> getConflictingDifferences(List<Diff> nonConflictingDiffs,
List<Conflict> realConflicts) {
ArrayList<Diff> conflictingDiffs = Lists.newArrayList();
for (Conflict conflict : realConflicts) {
EList<Diff> conflctDiffs = conflict.getDifferences();
conflictingDiffs.addAll(conflctDiffs);
}
// assert no overlap between non-conflicting and conflicting
assertFalse(nonConflictingDiffs.removeAll(conflictingDiffs));
assertFalse(conflictingDiffs.removeAll(nonConflictingDiffs));
return conflictingDiffs;
}
/**
* Sets the cascading filter option (subdiff-awareness) of all mergers to the given state. Any changes
* done by this method can be restored by calling {@link #restoreCascadingFilter()}.
*
* @param enabled
* filter state
*/
protected static void setCascadingFilter(boolean enabled) {
for (IMergeOptionAware merger : Iterables.filter(MERGER_REGISTRY.getMergers(null),
IMergeOptionAware.class)) {
Map<Object, Object> mergeOptions = merger.getMergeOptions();
Object previousValue = mergeOptions.get(SUB_DIFF_AWARE_OPTION);
CACHED_OPTIONS.put(merger, previousValue);
mergeOptions.put(SUB_DIFF_AWARE_OPTION, Boolean.valueOf(enabled));
}
}
/**
* Restores the cascading filter options changed by the last call to {@link #enableCascadingFilter()},
* {@link #disableCascadingFilter()}, or {@link #setCascadingFilter(boolean)}.
*/
protected static void restoreCascadingFilter() {
// restore previous values
for (Entry<IMergeOptionAware, Object> entry : CACHED_OPTIONS.entrySet()) {
IMergeOptionAware merger = entry.getKey();
merger.getMergeOptions().put(SUB_DIFF_AWARE_OPTION, entry.getValue());
}
}
}