Bug 529897 - [Compare] Log spammed with ClassCastExceptions during compare

Ensure that adapters added to the Resource dynamic proxies see those
same proxies in their call-backs, including in Notifications.

Change-Id: I69a1700506837ba33870222691e0424bc95296fb
diff --git a/plugins/compare/bundles/org.eclipse.papyrus.compare.uml2/src/org/eclipse/papyrus/compare/uml2/internal/hook/migration/ModelSetWrapper.java b/plugins/compare/bundles/org.eclipse.papyrus.compare.uml2/src/org/eclipse/papyrus/compare/uml2/internal/hook/migration/ModelSetWrapper.java
index d63cb2d..6b521ee 100644
--- a/plugins/compare/bundles/org.eclipse.papyrus.compare.uml2/src/org/eclipse/papyrus/compare/uml2/internal/hook/migration/ModelSetWrapper.java
+++ b/plugins/compare/bundles/org.eclipse.papyrus.compare.uml2/src/org/eclipse/papyrus/compare/uml2/internal/hook/migration/ModelSetWrapper.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2016, 2017 EclipseSource Services GmbH and others.
+ * Copyright (c) 2016, 2018 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
@@ -7,12 +7,15 @@
  * 
  * Contributors:
  *     Martin Fleck - initial API and implementation
- *     Christian W. Damus - bug 526932
- *     Christian W. Damus - bug 512529
+ *     Christian W. Damus - bugs 526932, 512529, 529897
  *******************************************************************************/
 package org.eclipse.papyrus.compare.uml2.internal.hook.migration;
 
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.MapMaker;
 
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
@@ -20,11 +23,16 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 
 import org.eclipse.emf.common.notify.Adapter;
 import org.eclipse.emf.common.notify.Notification;
+import org.eclipse.emf.common.notify.NotificationWrapper;
 import org.eclipse.emf.common.notify.Notifier;
+import org.eclipse.emf.common.notify.impl.AdapterImpl;
+import org.eclipse.emf.common.util.DelegatingEList;
+import org.eclipse.emf.common.util.EList;
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.EPackage.Registry;
 import org.eclipse.emf.ecore.resource.ContentHandler;
@@ -424,6 +432,8 @@
 	private class ResourceProxy implements InvocationHandler {
 		private final Resource resource;
 
+		private EList<Adapter> adapterList;
+
 		ResourceProxy(Resource resource) {
 			super();
 
@@ -444,10 +454,230 @@
 						return null;
 					}
 					break;
+				case "eAdapters": //$NON-NLS-1$
+					return getAdapters(proxy);
 			}
 
 			return method.invoke(resource, args);
 		}
 
+		EList<Adapter> getAdapters(Object proxy) {
+			if (adapterList == null) {
+				adapterList = new ProxyAdapterList(resource, (Resource)proxy);
+			}
+			return adapterList;
+		}
 	}
+
+	/**
+	 * An adapter list implementation for {@link ResourceProxy} instances to ensure that {@link Adapter}
+	 * call-backs are invoked with the resource proxy that the adapters are added to, not the underlying real
+	 * resource.
+	 *
+	 * @author Christian W. Damus
+	 */
+	@SuppressWarnings("serial")
+	private class ProxyAdapterList extends DelegatingEList<Adapter> implements EObservableAdapterList {
+
+		// Map of "real adapter" <--> wrapper
+		private final BiMap<Adapter, Adapter> adapters = HashBiMap.create();
+
+		private final Notifier owner;
+
+		private final Notifier proxy;
+
+		private final EList<Adapter> delegate;
+
+		private List<Listener> listeners;
+
+		/**
+		 * Initializes me with the real resource {@link notifier} and its {@code proxy} that I present to
+		 * adapters in their call-backs.
+		 * 
+		 * @param notifier
+		 *            the real notifier
+		 * @param proxy
+		 *            its proxy wrapper
+		 */
+		ProxyAdapterList(Notifier notifier, Notifier proxy) {
+			super();
+
+			owner = notifier;
+			this.proxy = proxy;
+			delegate = notifier.eAdapters();
+		}
+
+		@Override
+		protected List<Adapter> delegateList() {
+			return delegate;
+		}
+
+		@Override
+		protected boolean delegateContains(Object object) {
+			// The delegate list contains wrappers, not the real adapters
+			Object search = adapters.get(object);
+			return super.delegateContains(search == null ? object : search);
+		}
+
+		@Override
+		protected int delegateIndexOf(Object object) {
+			// The delegate list contains wrappers, not the real adapters.
+			// This is needed to support removal
+			Object search = adapters.get(object);
+			return super.delegateIndexOf(search == null ? object : search);
+		}
+
+		@Override
+		protected int delegateLastIndexOf(Object object) {
+			// The delegate list contains wrappers, not the real adapters.
+			// This is needed as counterpart to indexOf
+			Object search = adapters.get(object);
+			return super.delegateLastIndexOf(search == null ? object : search);
+		}
+
+		@Override
+		protected Adapter validate(int index, Adapter object) {
+			Adapter result = adapters.get(object);
+			if (result == null) {
+				result = new ProxyAdapter(object, owner, proxy);
+				adapters.put(object, result);
+			}
+
+			return result;
+		}
+
+		@Override
+		protected void didAdd(int index, Adapter newObject) {
+			if (listeners != null) {
+				for (Listener next : listeners) {
+					next.added(proxy, newObject);
+				}
+			}
+		}
+
+		@Override
+		protected void didRemove(int index, Adapter oldObject) {
+			if (listeners != null) {
+				for (Listener next : listeners) {
+					next.removed(proxy, oldObject);
+				}
+			}
+			adapters.inverse().remove(oldObject);
+		}
+
+		@Override
+		protected void didSet(int index, Adapter newObject, Adapter oldObject) {
+			didRemove(index, oldObject);
+			didAdd(index, newObject);
+		}
+
+		//
+		// EObservableAdapterList protocol
+		//
+
+		public void addListener(Listener listener) {
+			if (listeners == null) {
+				listeners = Lists.newArrayListWithExpectedSize(1);
+			}
+			listeners.add(listener);
+		}
+
+		public void removeListener(Listener listener) {
+			if (listeners != null) {
+				listeners.remove(listener);
+			}
+		}
+	}
+
+	//
+	// Nested types
+	//
+
+	/**
+	 * An wrapper for adapters to ensure that the {@link Adapter} call-backs are invoked with the notifier
+	 * proxy that the adapters are added to, not the underlying real notifier.
+	 *
+	 * @author Christian W. Damus
+	 */
+	private class ProxyAdapter extends AdapterImpl {
+		private final Adapter delegate;
+
+		private final Notifier notifier;
+
+		private final Notifier proxy;
+
+		/** Cache of notification wrappers for presentation of the notifier as the proxy. */
+		private final Map<Notification, Notification> notificationWrappers = new MapMaker().weakKeys()
+				.weakValues().makeMap();
+
+		/**
+		 * Initializes me with the real resource {@link notifier} and its {@code proxy} that I present to my
+		 * wrapper {@link adapter} in its call-backs.
+		 * 
+		 * @param adapter
+		 *            my real adapter delegate
+		 * @param notifier
+		 *            the real notifier
+		 * @param proxy
+		 *            its proxy wrapper
+		 */
+		ProxyAdapter(Adapter adapter, Notifier notifier, Notifier proxy) {
+			super();
+
+			delegate = adapter;
+			this.notifier = notifier;
+			this.proxy = proxy;
+		}
+
+		@Override
+		public void notifyChanged(Notification msg) {
+			delegate.notifyChanged(wrap(msg));
+		}
+
+		Notification wrap(Notification msg) {
+			Notification result = msg;
+
+			if (msg.getNotifier() == notifier) {
+				// Wrap it
+				result = notificationWrappers.get(msg);
+				if (result == null) {
+					result = new NotificationWrapper(proxy, msg);
+					notificationWrappers.put(msg, result);
+				}
+			}
+
+			return result;
+		}
+
+		@Override
+		public boolean isAdapterForType(Object type) {
+			return delegate.isAdapterForType(type);
+		}
+
+		@Override
+		public void setTarget(Notifier newTarget) {
+			if (newTarget == notifier) {
+				delegate.setTarget(proxy);
+			} else {
+				delegate.setTarget(newTarget);
+			}
+		}
+
+		@Override
+		public void unsetTarget(Notifier oldTarget) {
+			if (delegate instanceof Adapter.Internal) {
+				if (oldTarget == notifier) {
+					((Adapter.Internal)delegate).unsetTarget(proxy);
+				} else {
+					((Adapter.Internal)delegate).unsetTarget(oldTarget);
+				}
+			}
+		}
+
+		@Override
+		public String toString() {
+			return delegate.toString();
+		}
+	}
+
 }
diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/.settings/org.eclipse.core.resources.prefs b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..99f26c0
--- /dev/null
+++ b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/.settings/org.eclipse.core.runtime.prefs b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..5a0ad22
--- /dev/null
+++ b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/.settings/org.eclipse.jdt.ui.prefs b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..5766025
--- /dev/null
+++ b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,66 @@
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_EMF Compare
+formatter_settings_version=12
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=fr;com;java;javax;org;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=true
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_missing_override_annotations_interface_methods=false
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_functional_interfaces=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=true
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.insert_inferred_type_arguments=false
+sp_cleanup.make_local_variable_final=false
+sp_cleanup.make_parameters_final=false
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=true
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_redundant_type_arguments=false
+sp_cleanup.remove_trailing_whitespaces=false
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=true
+sp_cleanup.remove_unused_imports=true
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_anonymous_class_creation=false
+sp_cleanup.use_blocks=true
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_lambda=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+sp_cleanup.use_type_arguments=false
diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/src/org/eclipse/papyrus/compare/uml2/tests/profiles/migration/ModelSetWrapperTest.java b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/src/org/eclipse/papyrus/compare/uml2/tests/profiles/migration/ModelSetWrapperTest.java
new file mode 100644
index 0000000..8b7e8e6
--- /dev/null
+++ b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/src/org/eclipse/papyrus/compare/uml2/tests/profiles/migration/ModelSetWrapperTest.java
@@ -0,0 +1,315 @@
+/*******************************************************************************
+ * Copyright (c) 2018 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.uml2.tests.profiles.migration;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeThat;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
+import java.lang.reflect.Proxy;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.eclipse.emf.common.notify.Adapter;
+import org.eclipse.emf.common.notify.Notification;
+import org.eclipse.emf.common.notify.Notifier;
+import org.eclipse.emf.common.notify.impl.AdapterImpl;
+import org.eclipse.emf.common.util.URI;
+import org.eclipse.emf.ecore.EObject;
+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.emf.ecore.util.EcoreUtil;
+import org.eclipse.papyrus.compare.uml2.internal.hook.migration.ModelSetWrapper;
+import org.eclipse.uml2.uml.Profile;
+import org.eclipse.uml2.uml.resource.UMLResource;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link ModelSetWrapper} class.
+ *
+ * @author Christian W. Damus
+ */
+@SuppressWarnings({"nls", "boxing" })
+public class ModelSetWrapperTest {
+
+	private static final URI STANDARD_PROFILE_URI = URI.createURI(UMLResource.STANDARD_PROFILE_URI);
+
+	private ResourceSet fixture;
+
+	/**
+	 * Initializes me.
+	 */
+	public ModelSetWrapperTest() {
+		super();
+	}
+
+	@Test
+	public void resourceWrapper() {
+		Resource resource = fixture.getResource(STANDARD_PROFILE_URI, true);
+
+		assertThat(resource, isProxy());
+		assertThat(resource.getURI(), is(STANDARD_PROFILE_URI));
+		assertThat(resource.isLoaded(), is(true));
+		assertThat(resource.getContents(), CoreMatchers.<EObject> hasItem(instanceOf(Profile.class)));
+	}
+
+	@Test
+	public void notifications() {
+		final ListMultimap<URI, String> invocations = ArrayListMultimap.create();
+
+		class TestAdapter extends AdapterImpl {
+			private Pattern invocationPattern = Pattern.compile("(.+?)( x(\\d+))?");
+
+			@Override
+			public boolean isAdapterForType(Object type) {
+				if (getTarget() instanceof Resource) {
+					recordInvocation(getTarget(), "isAdapter");
+				}
+				return type == ModelSetWrapperTest.class;
+			}
+
+			void recordInvocation(Object context, String label) {
+				if (context instanceof Notification) {
+					recordInvocation(((Notification)context).getNotifier(), label);
+				} else if (context instanceof URI) {
+					recordInvocation((URI)context, label);
+				} else if (context instanceof Resource) {
+					recordInvocation(((Resource)context).getURI(), label);
+				} else {
+					recordInvocation(null, label);
+				}
+			}
+
+			void recordInvocation(URI context, String label) {
+				if (!invocations.containsKey(context)) {
+					invocations.put(context, label);
+				} else {
+					final List<String> these = invocations.get(context);
+					int lastIndex = these.size() - 1;
+					String last = these.get(lastIndex);
+					java.util.regex.Matcher m = invocationPattern.matcher(last);
+
+					if (!m.matches() || !m.group(1).equals(label)) {
+						these.add(label);
+					} else if (m.group(3) != null) {
+						// Increment the count
+						int count = Integer.parseInt(m.group(3));
+						these.set(lastIndex, String.format("%s x%s", label, count + 1));
+					} else {
+						// Start the count
+						these.set(lastIndex, String.format("%s x2", label));
+					}
+				}
+			}
+
+			@Override
+			public void notifyChanged(Notification msg) {
+				if (msg.getNotifier() instanceof Resource) {
+					if (msg.getEventType() == Notification.REMOVING_ADAPTER) {
+						recordInvocation(msg, "notifyChanged: removingAdapter");
+					} else {
+						switch (msg.getFeatureID(Resource.class)) {
+							case Resource.RESOURCE__IS_LOADED:
+								if (msg.getNewBooleanValue()) {
+									recordInvocation(msg, "loaded");
+								} else {
+									recordInvocation(msg, "unloaded");
+								}
+								break;
+							case Resource.RESOURCE__CONTENTS:
+							case Resource.RESOURCE__IS_TRACKING_MODIFICATION:
+							case Resource.RESOURCE__IS_MODIFIED:
+								recordInvocation(msg, "notifyChanged: "
+										+ resourceFeature(msg.getFeatureID(Resource.class)));
+								break;
+							default:
+								// Pass
+								break;
+						}
+					}
+					assertThat(msg.getNotifier(), isProxy());
+				} else if (msg.getNotifier() instanceof ResourceSet) {
+					handleResourceSet(msg);
+				}
+			}
+
+			@Override
+			public void setTarget(Notifier newTarget) {
+				// only track resources
+				if (newTarget instanceof Resource) {
+					recordInvocation(newTarget, "setTarget");
+					assertThat(newTarget, isProxy());
+					super.setTarget(newTarget);
+				}
+			}
+
+			@Override
+			public void unsetTarget(Notifier oldTarget) {
+				// only track resources
+				if (oldTarget instanceof Resource) {
+					recordInvocation(oldTarget, "unsetTarget");
+					assertThat(oldTarget, isProxy());
+					super.unsetTarget(oldTarget);
+				}
+			}
+
+			protected void handleResourceSet(Notification msg) {
+				switch (msg.getFeatureID(ResourceSet.class)) {
+					case ResourceSet.RESOURCE_SET__RESOURCES:
+						switch (msg.getEventType()) {
+							case Notification.ADD:
+								((Resource)msg.getNewValue()).eAdapters().add(this);
+								break;
+							case Notification.ADD_MANY:
+								for (Object next : (Collection<?>)msg.getNewValue()) {
+									((Resource)next).eAdapters().add(this);
+								}
+								break;
+							case Notification.REMOVE:
+								((Resource)msg.getOldValue()).eAdapters().remove(this);
+								break;
+							case Notification.REMOVE_MANY:
+								for (Object next : (Collection<?>)msg.getOldValue()) {
+									((Resource)next).eAdapters().remove(this);
+								}
+								break;
+							case Notification.SET:
+								((Resource)msg.getOldValue()).eAdapters().remove(this);
+								((Resource)msg.getNewValue()).eAdapters().add(this);
+								break;
+							default:
+								// Pass
+								break;
+						}
+						break;
+					default:
+						// Pass
+						break;
+				}
+
+			}
+		}
+
+		try {
+			fixture.eAdapters().add(new TestAdapter());
+
+			Resource resource = fixture.getResource(STANDARD_PROFILE_URI, true);
+			assumeThat(resource, isProxy());
+
+			Adapter adapter = EcoreUtil.getExistingAdapter(resource, ModelSetWrapperTest.class);
+			assertThat(adapter, notNullValue());
+			assertThat(resource.eAdapters(), hasItem(adapter));
+
+			resource.unload();
+			fixture.getResources().remove(resource);
+
+			// The adapter removes itself from the resource when the resource is
+			// removed from the set
+			assertThat(resource.eAdapters(), not(hasItem(adapter)));
+		} catch (UndeclaredThrowableException e) {
+			for (Throwable unwrap = e.getUndeclaredThrowable(); unwrap != null; unwrap = unwrap.getCause()) {
+				if (unwrap instanceof Error) {
+					throw (Error)unwrap; // Usually AssertionError
+				}
+			}
+			throw e; // Re-throw
+		}
+
+		assertThat(invocations.get(STANDARD_PROFILE_URI), is(Arrays.asList( //
+				"setTarget", //
+				"notifyChanged: isTrackingModification", // because ModelSet does that
+				"notifyChanged: isModified x2", //
+				"notifyChanged: contents", // loaded all at once
+				"notifyChanged: isModified", //
+				"loaded", //
+				"notifyChanged: isModified x3", // unloading
+				"notifyChanged: contents", // unloaded all at once
+				"unloaded", //
+				"notifyChanged: removingAdapter", //
+				"unsetTarget" //
+		)));
+	}
+
+	//
+	// Test framework
+	//
+
+	@Before
+	public void createFixture() {
+		ResourceSet rset = new ResourceSetImpl();
+		fixture = new ModelSetWrapper(rset);
+	}
+
+	@After
+	public void destroyFixture() {
+		ModelSetWrapper wrapper = (ModelSetWrapper)fixture;
+		for (Resource next : wrapper.getResources()) {
+			next.unload();
+		}
+		wrapper.getResources().clear();
+		wrapper.detach();
+	}
+
+	Matcher<Object> isProxy() {
+		return new BaseMatcher<Object>() {
+			public void describeTo(Description description) {
+				description.appendText("is a Java proxy");
+			}
+
+			public boolean matches(Object item) {
+				return item != null && Proxy.isProxyClass(item.getClass());
+			}
+		};
+	}
+
+	static String resourceFeature(int featureID) {
+		switch (featureID) {
+			case Resource.RESOURCE__CONTENTS:
+				return "contents";
+			case Resource.RESOURCE__ERRORS:
+				return "errors";
+			case Resource.RESOURCE__IS_LOADED:
+				return "isLoaded";
+			case Resource.RESOURCE__IS_MODIFIED:
+				return "isModified";
+			case Resource.RESOURCE__IS_TRACKING_MODIFICATION:
+				return "isTrackingModification";
+			case Resource.RESOURCE__RESOURCE_SET:
+				return "resourceSet";
+			case Resource.RESOURCE__TIME_STAMP:
+				return "timestamp";
+			case Resource.RESOURCE__URI:
+				return "uri";
+			case Resource.RESOURCE__WARNINGS:
+				return "warnings";
+			default:
+				return "<other>";
+		}
+	}
+}
diff --git a/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/src/org/eclipse/papyrus/compare/uml2/tests/suite/AllUITests.java b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/src/org/eclipse/papyrus/compare/uml2/tests/suite/AllUITests.java
index e1c5ce2..a6fa724 100644
--- a/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/src/org/eclipse/papyrus/compare/uml2/tests/suite/AllUITests.java
+++ b/plugins/compare/tests/org.eclipse.papyrus.compare.uml2.tests/src/org/eclipse/papyrus/compare/uml2/tests/suite/AllUITests.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2016 EclipseSource Services GmbH and others.
+ * Copyright (c) 2016, 2018 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
@@ -7,23 +7,25 @@
  * 
  * Contributors:
  *     Martin Fleck - initial API and implementation
+ *     Christian W. Damus - bug 529897
  *******************************************************************************/
 package org.eclipse.papyrus.compare.uml2.tests.suite;
 
+import org.eclipse.papyrus.compare.uml2.tests.profiles.migration.ModelSetWrapperTest;
 import org.eclipse.papyrus.compare.uml2.tests.profiles.migration.ProfileMigrationTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 import org.junit.runners.Suite.SuiteClasses;
 
 /**
- * This test suite allows us to launch all tests for EMF Compare for UML at once. This test suite should
- * contain classes that need the UI in order to be executed. Tests that do not need the UI should go to
- * {@link AllTests}.
+ * Test suite for the Papyrus Compare UML2 tests.
  * 
  * @author Martin Fleck <mfleck@eclipsesource.com>
  */
 @RunWith(Suite.class)
-@SuiteClasses({ProfileMigrationTest.class })
+@SuiteClasses({ProfileMigrationTest.class, //
+		ModelSetWrapperTest.class, //
+})
 public class AllUITests {
 
 }