| /* |
| * Copyright (c) 2014, 2015 CEA, 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 (CEA) - Initial API and implementation |
| * Christian W. Damus - bug 476683 |
| * |
| */ |
| package org.eclipse.papyrus.junit.utils.rules; |
| |
| import static java.lang.annotation.ElementType.FIELD; |
| import static java.lang.annotation.RetentionPolicy.RUNTIME; |
| import static org.hamcrest.CoreMatchers.is; |
| import static org.hamcrest.CoreMatchers.notNullValue; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.junit.Assert.fail; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.Target; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| import org.eclipse.core.commands.operations.IUndoableOperation; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.emf.common.command.Command; |
| import org.eclipse.emf.common.util.WrappedException; |
| 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.edit.domain.EditingDomain; |
| import org.eclipse.emf.transaction.TransactionalEditingDomain; |
| import org.eclipse.gef.EditPart; |
| import org.eclipse.gmf.runtime.common.core.command.ICommand; |
| import org.eclipse.papyrus.infra.core.resource.ModelSet; |
| import org.eclipse.papyrus.infra.emf.utils.EMFHelper; |
| import org.eclipse.papyrus.infra.ui.editor.IMultiDiagramEditor; |
| import org.eclipse.papyrus.junit.utils.Duck; |
| import org.eclipse.papyrus.junit.utils.EditorUtils; |
| import org.eclipse.papyrus.junit.utils.PapyrusProjectUtils; |
| import org.eclipse.papyrus.junit.utils.ProjectUtils; |
| import org.eclipse.papyrus.junit.utils.rules.HouseKeeper.Disposer; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.ui.IEditorPart; |
| import org.eclipse.ui.IWorkbenchPage; |
| import org.osgi.framework.FrameworkUtil; |
| |
| import com.google.common.base.Function; |
| import com.google.common.base.Functions; |
| import com.google.common.cache.CacheBuilder; |
| import com.google.common.cache.CacheLoader; |
| import com.google.common.cache.LoadingCache; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| |
| public abstract class AbstractHouseKeeperRule { |
| |
| private static final LoadingCache<Class<?>, Field[]> leakProneInstanceFields = CacheBuilder.newBuilder().maximumSize(128).build(fieldCacheLoader(false)); |
| |
| private static final LoadingCache<Class<?>, Field[]> leakProneStaticFields = CacheBuilder.newBuilder().maximumSize(128).build(fieldCacheLoader(true)); |
| |
| private static final Function<Object, Disposer<Object>> DISPOSER_FUNCTION; |
| |
| Object test; |
| |
| String testName; |
| |
| private List<Runnable> cleanUpActions; |
| |
| static { |
| final Map<Class<?>, Function<Object, Disposer<?>>> disposers = Maps.newLinkedHashMap(); |
| |
| ResourceSetDisposer.register(disposers); |
| TransactionalEditingDomainDisposer.register(disposers); |
| WorkspaceResourceDisposer.register(disposers); |
| EditorDisposer.register(disposers); |
| CollectionDisposer.register(disposers); |
| MapDisposer.register(disposers); |
| |
| // This one must be last because it matches any object |
| ReflectiveDisposer.register(disposers); |
| |
| DISPOSER_FUNCTION = new Function<Object, Disposer<Object>>() { |
| |
| private final Function<Object, Disposer<?>> nullFunction = Functions.constant(null); |
| |
| @Override |
| public Disposer<Object> apply(Object input) { |
| Function<Object, Disposer<?>> resultFunction = nullFunction; |
| |
| for (Map.Entry<Class<?>, Function<Object, Disposer<?>>> next : disposers.entrySet()) { |
| if (next.getKey().isInstance(input)) { |
| resultFunction = next.getValue(); |
| break; |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| Disposer<Object> result = (Disposer<Object>) resultFunction.apply(input); |
| return result; |
| } |
| }; |
| } |
| |
| AbstractHouseKeeperRule() { |
| super(); |
| } |
| |
| /** |
| * Obtains the test name (may as well provide it, since we are a test rule). |
| * |
| * @return the current test name |
| */ |
| public final String getTestName() { |
| return testName; |
| } |
| |
| /** |
| * Adds an {@code object} to clean up later, with a {@code disposer} method that is invoked reflectively to do the cleaning up. |
| * |
| * @param object |
| * an object to dispose after the test has completed |
| * @param disposer |
| * the disposal method name |
| * @param arg |
| * arguments (if any) to the {@code disposer} method |
| * |
| * @return the {@code object}, for convenience |
| */ |
| public <T> T cleanUpLater(T object, String disposer, Object... arg) { |
| assertThat("No such disposal method", new Duck(object).understands(disposer, arg), is(true)); |
| return cleanUpLater(object, new ReflectiveDisposer(disposer, arg)); |
| } |
| |
| /** |
| * Adds an {@code object} to clean up later, with a {@code disposer} that does the cleaning up. |
| * |
| * @param object |
| * an object to dispose after the test has completed |
| * @param disposer |
| * the disposal behaviour |
| * |
| * @return the {@code object}, for convenience |
| */ |
| public <T> T cleanUpLater(T object, Disposer<? super T> disposer) { |
| if (cleanUpActions == null) { |
| cleanUpActions = Lists.newLinkedList(); |
| } |
| |
| // Clean up in reverse order to best manage dependencies between cleaned-up objects |
| cleanUpActions.add(0, new CleanUpAction(object, disposer)); |
| return object; |
| } |
| |
| /** |
| * Adds an {@code object} to clean up later, using the appropriate built-in disposer. |
| * Fails if the {@code object} does not have a corresponding built-in disposer. |
| * |
| * @param object |
| * an object to dispose after the test has completed |
| * |
| * @return the {@code object}, for convenience |
| */ |
| public <T> T cleanUpLater(T object) { |
| @SuppressWarnings("unchecked") |
| Disposer<T> disposer = (Disposer<T>) DISPOSER_FUNCTION.apply(object); |
| assertThat("No built-in disposer available", disposer, notNullValue()); |
| return cleanUpLater(object, disposer); |
| } |
| |
| /** |
| * Obtains a new resource set that will be disposed of automatically after the test completes. |
| * |
| * @return the new resource set |
| */ |
| public ResourceSet createResourceSet() { |
| return cleanUpLater(new ResourceSetImpl(), ResourceSetDisposer.INSTANCE); |
| } |
| |
| /** |
| * Creates a new editing domain that will be disposed of automatically after the test completes. |
| * |
| * @return the editing domain |
| */ |
| public TransactionalEditingDomain createSimpleEditingDomain() { |
| return createSimpleEditingDomain(null); |
| } |
| |
| /** |
| * Creates a new editing domain that will be disposed of automatically after the test completes. |
| * |
| * @param resourceSet |
| * the resource set on which to create the editing domain (or {@code null} to create a default one) |
| * |
| * @return the editing domain |
| */ |
| public TransactionalEditingDomain createSimpleEditingDomain(ResourceSet resourceSet) { |
| if (resourceSet == null) { |
| resourceSet = createResourceSet(); |
| } |
| |
| return cleanUpLater(TransactionalEditingDomain.Factory.INSTANCE.createEditingDomain(resourceSet), TransactionalEditingDomainDisposer.INSTANCE); |
| } |
| |
| /** |
| * Creates a project that will be disposed of automatically after the test completes. |
| * |
| * @param name |
| * the name of the project |
| * |
| * @return the project |
| */ |
| public IProject createProject(String name) { |
| try { |
| return cleanUpLater(ProjectUtils.createProject(name), WorkspaceResourceDisposer.INSTANCE); |
| } catch (Exception e) { |
| fail(e.getMessage()); |
| return null; // Unreachable |
| } |
| } |
| |
| /** |
| * Creates a file in the specified {@code project} with the given {@code fileName}, initialized by copying a |
| * template resource from the test class's originating bundle. |
| * |
| * @param project |
| * the test project in which to create the file |
| * @param fileName |
| * the name of the file to create |
| * @param templatePath |
| * the path in the test bundle of the template file to copy |
| * |
| * @return the new file |
| */ |
| public IFile createFile(IProject project, String fileName, String templatePath) { |
| Class<?> testClass = (test instanceof Class<?>) ? (Class<?>) test : test.getClass(); |
| |
| try { |
| return cleanUpLater(PapyrusProjectUtils.copyIFile(templatePath, FrameworkUtil.getBundle(testClass), project, fileName), // |
| WorkspaceResourceDisposer.INSTANCE); |
| } catch (Exception e) { |
| fail(e.getMessage()); |
| return null; // Unreachable |
| } |
| } |
| |
| /** |
| * Opens the default editor on the given {@code file} and ensures that it will be closed after the test terminates. |
| * |
| * @param file |
| * the file to open in its editor |
| * |
| * @return the editor |
| */ |
| public IEditorPart openEditor(final IFile file) { |
| final IEditorPart[] result = { null }; |
| |
| Display.getDefault().syncExec(new Runnable() { |
| |
| @Override |
| public void run() { |
| try { |
| result[0] = cleanUpLater(EditorUtils.openEditor(file), EditorDisposer.INSTANCE); |
| } catch (Exception e) { |
| fail(e.getMessage()); |
| } |
| } |
| }); |
| |
| return result[0]; |
| } |
| |
| /** |
| * Opens the Papyrus editor on the given {@code file} and ensures that it will be closed after the test terminates. |
| * |
| * @param file |
| * the file to open in the Papyrus editor |
| * |
| * @return the editor |
| */ |
| public IMultiDiagramEditor openPapyrusEditor(final IFile file) throws Exception { |
| final IMultiDiagramEditor[] result = { null }; |
| final AtomicReference<Exception> syncExecException = new AtomicReference<Exception>(); |
| |
| Display.getDefault().syncExec(new Runnable() { |
| |
| @Override |
| public void run() { |
| try { |
| result[0] = cleanUpLater(EditorUtils.openPapyrusEditor(file), EditorDisposer.INSTANCE); |
| } catch (Exception ex) { |
| syncExecException.set(ex); |
| } |
| } |
| }); |
| |
| if (syncExecException.get() != null) { |
| throw syncExecException.get(); |
| } |
| |
| return result[0]; |
| } |
| |
| /** |
| * Obtains the value of the named field of the test instance and ensures that it will be automatically cleared after the test completes. |
| * |
| * @param fieldName |
| * the field to access now and clear later |
| * |
| * @return the value of the field |
| * |
| * @deprecated Use the {@link CleanUp @CleanUp} annotation on the field and access it directly. |
| */ |
| @Deprecated |
| public <T> T getField(String fieldName) { |
| try { |
| Field field = field(fieldName); |
| |
| @SuppressWarnings("unchecked") |
| T result = (T) field.get(test); |
| cleanUpLater(field, new FieldDisposer()); |
| |
| return result; |
| } catch (Exception e) { |
| e.printStackTrace(); |
| fail(String.format("Could not access field %s of test instance.", fieldName)); |
| return null; // Unreachable |
| } |
| } |
| |
| Field field(String fieldName) { |
| Field result = null; |
| |
| for (Class<?> next = getTestClass(); (result == null) && (next != null) && (next != Object.class); next = next.getSuperclass()) { |
| try { |
| result = next.getDeclaredField(fieldName); |
| if (result != null) { |
| result.setAccessible(true); |
| } |
| } catch (Exception e) { |
| // Keep looking |
| result = null; |
| } |
| } |
| |
| assertThat(String.format("Could not access field %s of test instance.", fieldName), result, notNullValue()); |
| assertThat(String.format("Field is not %sstatic", isStatic() ? "" : "non-"), Modifier.isStatic(result.getModifiers()), is(isStatic())); |
| |
| return result; |
| } |
| |
| /** |
| * Sets the value of the named field of the test instance and ensures that it will be automatically cleared after the test completes. |
| * |
| * @param fieldName |
| * the field to access now and clear later |
| * @param value |
| * the value to set |
| * |
| * @return the new value of the field |
| * |
| * @deprecated Use the {@link CleanUp @CleanUp} annotation on the field and access it directly. |
| */ |
| @Deprecated |
| public <T> T setField(String fieldName, T value) { |
| try { |
| Field field = field(fieldName); |
| field.set(test, value); |
| cleanUpLater(field, new FieldDisposer()); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| fail(String.format("Could not access field %s of test instance.", fieldName)); |
| } |
| |
| return value; |
| } |
| |
| abstract boolean isStatic(); |
| |
| abstract Class<?> getTestClass(); |
| |
| |
| void registerAutoCleanups() { |
| try { |
| final boolean staticFields = isStatic(); |
| |
| // Get all inherited fields, too |
| for (Class<?> next = getTestClass(); (next != null) && (next != Object.class); next = next.getSuperclass()) { |
| for (Field field : next.getDeclaredFields()) { |
| CleanUp cleanUp = field.getAnnotation(CleanUp.class); |
| |
| if ((cleanUp != null) && (Modifier.isStatic(field.getModifiers()) == staticFields) && !Modifier.isFinal(field.getModifiers())) { |
| try { |
| field.setAccessible(true); |
| |
| Class<? extends Disposer<?>> disposerClass = cleanUp.value(); |
| if (disposerClass == FieldDisposer.class) { |
| // Default case |
| cleanUpLater(field, new FieldDisposer()); |
| } else { |
| // Custom case |
| |
| // Handle inner classes |
| Constructor<? extends Disposer<?>> ctor; |
| Object[] args; |
| if (disposerClass.getDeclaringClass() != null && ((disposerClass.getModifiers() & Modifier.STATIC) == 0)) { |
| ctor = disposerClass.getDeclaredConstructor(disposerClass.getDeclaringClass()); |
| args = new Object[] { this }; |
| } else { |
| ctor = disposerClass.getConstructor(); |
| args = new Object[0]; |
| } |
| ctor.setAccessible(true); |
| |
| @SuppressWarnings("unchecked") |
| Disposer<Object> disposer = (Disposer<Object>) ctor.newInstance(args); |
| cleanUpLater(field.get(test), disposer); |
| } |
| } catch (Exception e) { |
| // Can't make it accessible? Then it's of no use. |
| // Likewise any problem in creating the disposer |
| e.printStackTrace(); |
| } |
| } |
| } |
| } |
| } catch (Exception e) { |
| // We tried our best. Don't propagate as a test failure because the test didn't ask for this |
| } |
| } |
| |
| void cleanUp() throws Exception { |
| cleanUpLeakProneFields(); |
| |
| if (cleanUpActions != null) { |
| Exception toThrow = null; |
| |
| for (Runnable next : cleanUpActions) { |
| try { |
| next.run(); |
| } catch (Exception e) { |
| // Unwrap |
| if (e instanceof WrappedException) { |
| e = ((WrappedException) e).exception(); |
| } |
| |
| e.printStackTrace(); |
| if (toThrow == null) { |
| toThrow = e; |
| } |
| } |
| } |
| |
| cleanUpActions = null; |
| |
| if (toThrow != null) { |
| throw toThrow; |
| } |
| } |
| } |
| |
| /** |
| * Automatically clear all fields of the test instance that are of some {@link EObject} type. |
| */ |
| private void cleanUpLeakProneFields() { |
| try { |
| final Field[] fields = isStatic() ? leakProneStaticFields.get(getTestClass()) : leakProneInstanceFields.get(getTestClass()); |
| |
| for (int i = 0; i < fields.length; i++) { |
| fields[i].set(test, null); |
| } |
| } catch (Exception e) { |
| // We tried our best. Don't propagate as a test failure because the test didn't ask for this |
| } |
| } |
| |
| private static CacheLoader<Class<?>, Field[]> fieldCacheLoader(final boolean staticFields) { |
| return new CacheLoader<Class<?>, Field[]>() { |
| |
| @Override |
| public Field[] load(Class<?> key) { |
| List<Field> result = Lists.newArrayList(); |
| |
| // Get all inherited fields, too |
| for (Class<?> next = key; (next != null) && (next != Object.class); next = next.getSuperclass()) { |
| for (Field field : next.getDeclaredFields()) { |
| if ((Modifier.isStatic(field.getModifiers()) == staticFields) && !Modifier.isFinal(field.getModifiers()) && isLeakProne(field)) { |
| try { |
| field.setAccessible(true); |
| result.add(field); |
| } catch (Exception e) { |
| // Can't make it accessible? Then it's of no use |
| } |
| } |
| } |
| } |
| |
| return Iterables.toArray(result, Field.class); |
| } |
| }; |
| } |
| |
| private static boolean isLeakProne(Field field) { |
| Class<?> type = field.getType(); |
| return EObject.class.isAssignableFrom(type) || Resource.class.isAssignableFrom(type) // |
| || ResourceSet.class.isAssignableFrom(type) || EditingDomain.class.isAssignableFrom(type) // |
| || EditPart.class.isAssignableFrom(type) // |
| || Command.class.isAssignableFrom(type) || org.eclipse.gef.commands.Command.class.isAssignableFrom(type) // |
| || IUndoableOperation.class.isAssignableFrom(type) || ICommand.class.isAssignableFrom(type); |
| } |
| |
| // |
| // Nested types |
| // |
| |
| /** |
| * Annotates fields for automatic clean-up. |
| */ |
| @Retention(RUNTIME) |
| @Target(FIELD) |
| public @interface CleanUp { |
| /** |
| * Optionally specifies a disposer class to instantiate to clean |
| * up the annotated field. By default, the field is simply |
| * cleared to {@code null}. |
| */ |
| Class<? extends Disposer<?>>value() default FieldDisposer.class; |
| } |
| |
| private static final class CleanUpAction implements Runnable { |
| |
| private final Object target; |
| |
| private final Disposer<Object> disposer; |
| |
| @SuppressWarnings("unchecked") |
| <T> CleanUpAction(T object, Disposer<? super T> disposer) { |
| this.target = object; |
| this.disposer = (Disposer<Object>) disposer; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| disposer.dispose(target); |
| } catch (Exception e) { |
| throw new WrappedException(e); |
| } |
| } |
| } |
| |
| private static final class ResourceSetDisposer implements Disposer<ResourceSet> { |
| |
| static final ResourceSetDisposer INSTANCE = new ResourceSetDisposer(); |
| |
| private ResourceSetDisposer() { |
| super(); |
| } |
| |
| static void register(Map<Class<?>, Function<Object, Disposer<?>>> disposers) { |
| disposers.put(ResourceSet.class, Functions.<Disposer<?>> constant(INSTANCE)); |
| } |
| |
| @Override |
| public void dispose(ResourceSet object) { |
| if (object instanceof ModelSet) { |
| ((ModelSet) object).unload(); |
| } |
| |
| // No harm in hitting a ModelSet again |
| EMFHelper.unload(object); |
| } |
| } |
| |
| private static final class TransactionalEditingDomainDisposer implements Disposer<TransactionalEditingDomain> { |
| |
| static final TransactionalEditingDomainDisposer INSTANCE = new TransactionalEditingDomainDisposer(); |
| |
| private TransactionalEditingDomainDisposer() { |
| super(); |
| } |
| |
| static void register(Map<Class<?>, Function<Object, Disposer<?>>> disposers) { |
| disposers.put(TransactionalEditingDomain.class, Functions.<Disposer<?>> constant(INSTANCE)); |
| } |
| |
| @Override |
| public void dispose(TransactionalEditingDomain object) { |
| object.dispose(); |
| } |
| } |
| |
| private final class FieldDisposer implements Disposer<Field> { |
| |
| @Override |
| public void dispose(Field object) throws Exception { |
| object.set(test, null); |
| } |
| } |
| |
| private static final class WorkspaceResourceDisposer implements Disposer<IResource> { |
| |
| static final WorkspaceResourceDisposer INSTANCE = new WorkspaceResourceDisposer(); |
| |
| static void register(Map<Class<?>, Function<Object, Disposer<?>>> disposers) { |
| disposers.put(IResource.class, Functions.<Disposer<?>> constant(INSTANCE)); |
| } |
| |
| @Override |
| public void dispose(IResource object) throws Exception { |
| switch (object.getType()) { |
| case IResource.PROJECT: |
| case IResource.FOLDER: |
| case IResource.FILE: |
| object.delete(true, null); |
| break; |
| default: |
| // Delete the workspace? No, I don't think so |
| fail("Cannot delete resource " + object); |
| break; |
| } |
| } |
| } |
| |
| private static final class EditorDisposer implements Disposer<IEditorPart> { |
| |
| static final EditorDisposer INSTANCE = new EditorDisposer(); |
| |
| static void register(Map<Class<?>, Function<Object, Disposer<?>>> disposers) { |
| disposers.put(IEditorPart.class, Functions.<Disposer<?>> constant(INSTANCE)); |
| } |
| |
| @Override |
| public void dispose(final IEditorPart object) throws Exception { |
| Display.getDefault().syncExec(new Runnable() { |
| |
| @Override |
| public void run() { |
| IWorkbenchPage page = (object.getSite() == null) ? null : object.getSite().getPage(); |
| if (page != null) { |
| try { |
| page.closeEditor(object, false); |
| } catch (Exception e) { |
| // Best effort |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| private static final class CollectionDisposer implements Disposer<Collection<?>> { |
| |
| static final CollectionDisposer INSTANCE = new CollectionDisposer(); |
| |
| static void register(Map<Class<?>, Function<Object, Disposer<?>>> disposers) { |
| disposers.put(Collection.class, Functions.<Disposer<?>> constant(INSTANCE)); |
| } |
| |
| @Override |
| public void dispose(final Collection<?> object) throws Exception { |
| object.clear(); |
| } |
| } |
| |
| private static final class MapDisposer implements Disposer<Map<?, ?>> { |
| |
| static final MapDisposer INSTANCE = new MapDisposer(); |
| |
| static void register(Map<Class<?>, Function<Object, Disposer<?>>> disposers) { |
| disposers.put(Map.class, Functions.<Disposer<?>> constant(INSTANCE)); |
| } |
| |
| @Override |
| public void dispose(final Map<?, ?> object) throws Exception { |
| object.clear(); |
| } |
| } |
| |
| private static final class ReflectiveDisposer implements Disposer<Object> { |
| |
| static final ReflectiveDisposer INSTANCE = new ReflectiveDisposer("dispose"); //$NON-NLS-1$ |
| |
| private final String disposeMethod; |
| |
| private final Object[] arguments; |
| |
| ReflectiveDisposer(String methodName, Object... arguments) { |
| this.disposeMethod = methodName; |
| this.arguments = arguments; |
| } |
| |
| static void register(Map<Class<?>, Function<Object, Disposer<?>>> disposers) { |
| disposers.put(Object.class, new Function<Object, Disposer<?>>() { |
| |
| @Override |
| public Disposer<?> apply(Object input) { |
| Duck duck = new Duck(input); |
| |
| return duck.understands(INSTANCE.disposeMethod, INSTANCE.arguments) ? INSTANCE : null; |
| } |
| }); |
| } |
| |
| @Override |
| public void dispose(Object object) throws Exception { |
| new Duck(object).quack(disposeMethod, arguments); |
| } |
| } |
| } |