diff options
author | Christian W. Damus | 2016-07-19 14:35:41 +0000 |
---|---|---|
committer | Christian W. Damus | 2016-07-19 15:16:16 +0000 |
commit | 1715bb61f46aa6251ff5bda74115e8e0530f5856 (patch) | |
tree | a8a06e1a00a199f90a97f0ae63e6e8bee58a1404 | |
parent | 68f29daa4d3427f79f12524790fd71f8431486bc (diff) | |
download | org.eclipse.papyrus-1715bb61f46aa6251ff5bda74115e8e0530f5856.tar.gz org.eclipse.papyrus-1715bb61f46aa6251ff5bda74115e8e0530f5856.tar.xz org.eclipse.papyrus-1715bb61f46aa6251ff5bda74115e8e0530f5856.zip |
Bug 498140: Memory leak in validation privileged runnables
https://bugs.eclipse.org/bugs/show_bug.cgi?id=498140
Forget the privileged runnable and its transaction context after
executing the wrapping runnable-with-progress. Clarify the one-shot
nature of the API.
Similar changes for the headless progress runnable/callable APIs
in the core.
Change-Id: I5ab1361032ea5c48e493b86823e46413f1d199d3
(cherry picked from commit 03dd4b0b4844e55d22563ba72eb8eaabb7a9ca49)
6 files changed, 457 insertions, 40 deletions
diff --git a/plugins/infra/core/org.eclipse.papyrus.infra.core/src/org/eclipse/papyrus/infra/core/utils/TransactionHelper.java b/plugins/infra/core/org.eclipse.papyrus.infra.core/src/org/eclipse/papyrus/infra/core/utils/TransactionHelper.java index 7f00384381a..90315f531b5 100644 --- a/plugins/infra/core/org.eclipse.papyrus.infra.core/src/org/eclipse/papyrus/infra/core/utils/TransactionHelper.java +++ b/plugins/infra/core/org.eclipse.papyrus.infra.core/src/org/eclipse/papyrus/infra/core/utils/TransactionHelper.java @@ -1,5 +1,5 @@ /***************************************************************************** - * Copyright (c) 2014, 2015 CEA LIST, Christian W. Damus, and others. + * Copyright (c) 2014, 2016 CEA LIST, 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 @@ -9,7 +9,7 @@ * Contributors: * Camille Letavernier (CEA LIST) camille.letavernier@cea.fr - Initial API and implementation * Christian W. Damus (CEA) - bugs 429826, 408491, 433320 - * Christian W. Damus - bugs 451557, 457560, 461629, 463564, 466997, 465416, 485220 + * Christian W. Damus - bugs 451557, 457560, 461629, 463564, 466997, 465416, 485220, 498140 * *****************************************************************************/ package org.eclipse.papyrus.infra.core.utils; @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.emf.edit.domain.EditingDomain; @@ -473,9 +474,16 @@ public class TransactionHelper { } /** + * <p> * Create a privileged progress runnable, which is like a regular {@linkplain TransactionalEditingDomain#createPrivilegedRunnable(Runnable) * privileged runnable} except that it is given a progress monitor for progress reporting. - * + * </p> + * <b>Note</b> that a privileged progress-runnable can be used only once. Because it has the + * context of a borrowed transaction, repeated execution is invalid because the required + * transaction context will have expired. Attempting to run a privileged runnable a second + * time will throw an {@link IllegalStateException}. + * </p> + * * @param domain * an editing domain * @param runnable @@ -488,16 +496,21 @@ public class TransactionHelper { Runnable privileged = domain.createPrivilegedRunnable(() -> runnable.run(monitorHolder[0])); - return monitor -> { - monitorHolder[0] = monitor; - privileged.run(); - }; + return new PrivilegedProgressRunnable(privileged, m -> monitorHolder[0] = m); } /** + * <p> * Create a privileged progress callable, which is like a {@linkplain TransactionalEditingDomain#createPrivilegedRunnable(Runnable) * privileged runnable} except that it is given a progress monitor for progress reporting and it computes a result. - * + * </p> + * <p> + * <b>Note</b> that a privileged progress-callable can be used only once. Because it has the + * context of a borrowed transaction, repeated execution is invalid because the required + * transaction context will have expired. Attempting to run a privileged callable a second + * time will throw an {@link IllegalStateException}. + * </p> + * * @param callable * an editing domain * @param callable @@ -506,22 +519,27 @@ public class TransactionHelper { * @since 2.0 */ public static <V> IProgressCallable<V> createPrivilegedCallable(TransactionalEditingDomain domain, final IProgressCallable<V> callable) { - IProgressMonitor monitorHolder[] = { null }; - AtomicReference<V> resultHolder = new AtomicReference<V>(); + AtomicReference<V> resultHolder = new AtomicReference<>(); Exception failHolder[] = { null }; - Runnable privileged = domain.createPrivilegedRunnable(() -> { + IProgressRunnable[] privileged = { createPrivilegedRunnable(domain, monitor -> { try { - resultHolder.set(callable.call(monitorHolder[0])); + resultHolder.set(callable.call(monitor)); } catch (Exception e) { failHolder[0] = e; } - }); + }) }; return monitor -> { - monitorHolder[0] = monitor; + if (privileged[0] == null) { + throw new IllegalStateException("Privileged callable was already run"); //$NON-NLS-1$ + } - privileged.run(); + try { + privileged[0].run(monitor); + } finally { + privileged[0] = null; + } if (failHolder[0] != null) { throw failHolder[0]; @@ -530,4 +548,41 @@ public class TransactionHelper { return resultHolder.get(); }; } + + // + // Nested types + // + + private static final class PrivilegedProgressRunnable implements IProgressRunnable { + private final Consumer<? super IProgressMonitor> monitorSlot; + private Runnable privileged; + + PrivilegedProgressRunnable(Runnable privileged, Consumer<? super IProgressMonitor> monitorSlot) { + super(); + + this.monitorSlot = monitorSlot; + this.privileged = privileged; + } + + @Override + public void run(IProgressMonitor monitor) { + if (privileged == null) { + throw new IllegalArgumentException("Privileged runnable was already run"); //$NON-NLS-1$ + } + + monitorSlot.accept(monitor); + + try { + privileged.run(); + } finally { + // Clear our reference to the runnable, which holds a reference + // to the transaction which, in turn, holds monitors. If we + // are run in the ModalContextThread and initialize an Xtext UI + // bundle, then that thread will be retained forever in its + // Guice injector as the creating thread, and that thread then + // will retain me (its runnable). See bug 498140 + privileged = null; + } + } + } } diff --git a/plugins/infra/ui/org.eclipse.papyrus.infra.ui/src/org/eclipse/papyrus/infra/ui/util/TransactionUIHelper.java b/plugins/infra/ui/org.eclipse.papyrus.infra.ui/src/org/eclipse/papyrus/infra/ui/util/TransactionUIHelper.java index 1fdc3344e41..6ee4ed2b14c 100644 --- a/plugins/infra/ui/org.eclipse.papyrus.infra.ui/src/org/eclipse/papyrus/infra/ui/util/TransactionUIHelper.java +++ b/plugins/infra/ui/org.eclipse.papyrus.infra.ui/src/org/eclipse/papyrus/infra/ui/util/TransactionUIHelper.java @@ -8,12 +8,13 @@ * * Contributors: * Camille Letavernier (CEA LIST) camille.letavernier@cea.fr - Initial API and implementation - * Christian W. Damus - bug 465416 + * Christian W. Damus - bugs 465416, 498140 * *****************************************************************************/ package org.eclipse.papyrus.infra.ui.util; import java.lang.reflect.InvocationTargetException; +import java.util.function.Consumer; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; @@ -30,10 +31,17 @@ import org.eclipse.jface.operation.IRunnableWithProgress; public class TransactionUIHelper { /** + * <p> * Create a privileged runnable with progress, which is like a regular {@linkplain TransactionalEditingDomain#createPrivilegedRunnable(Runnable) * privileged runnable} except that it is given a progress monitor for progress reporting. * This enables execution of monitored runnables on the modal-context thread using the transaction borrowed from the UI thread. - * + * </p> + * <b>Note</b> that a privileged progress-runnable can be used only once. Because it has the + * context of a borrowed transaction, repeated execution is invalid because the required + * transaction context will have expired. Attempting to run a privileged runnable a second + * time will throw an {@link IllegalStateException}. + * </p> + * * @param domain * an editing domain * @param runnable @@ -43,7 +51,7 @@ public class TransactionUIHelper { public static IRunnableWithProgress createPrivilegedRunnableWithProgress(TransactionalEditingDomain domain, final IRunnableWithProgress runnable) { final IProgressMonitor monitorHolder[] = { null }; - final Runnable privileged = domain.createPrivilegedRunnable(new Runnable() { + Runnable privileged = domain.createPrivilegedRunnable(new Runnable() { @Override public void run() { @@ -53,31 +61,60 @@ public class TransactionUIHelper { throw e; } catch (Exception e) { throw new WrappedException(e); + } finally { + monitorHolder[0] = null; } } }); - return new IRunnableWithProgress() { + return new PrivilegedRunnableWithProgress(privileged, m -> monitorHolder[0] = m); + } - @Override - public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { - monitorHolder[0] = monitor; + // + // Nested types + // - try { - privileged.run(); - } catch (OperationCanceledException e) { - throw new InterruptedException(e.getLocalizedMessage()); - } catch (WrappedException e) { - Exception unwrapped = e.exception(); - if (unwrapped instanceof InvocationTargetException) { - throw (InvocationTargetException) unwrapped; - } else if (unwrapped instanceof InterruptedException) { - throw (InterruptedException) unwrapped; - } else { - throw e; - } + private static final class PrivilegedRunnableWithProgress implements IRunnableWithProgress { + private final Consumer<? super IProgressMonitor> monitorSlot; + private Runnable privileged; + + PrivilegedRunnableWithProgress(Runnable privileged, Consumer<? super IProgressMonitor> monitorSlot) { + super(); + + this.monitorSlot = monitorSlot; + this.privileged = privileged; + } + + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + if (privileged == null) { + throw new IllegalStateException("Privileged runnable was already run"); //$NON-NLS-1$ + } + + monitorSlot.accept(monitor); + + try { + privileged.run(); + } catch (OperationCanceledException e) { + throw new InterruptedException(e.getLocalizedMessage()); + } catch (WrappedException e) { + Exception unwrapped = e.exception(); + if (unwrapped instanceof InvocationTargetException) { + throw (InvocationTargetException) unwrapped; + } else if (unwrapped instanceof InterruptedException) { + throw (InterruptedException) unwrapped; + } else { + throw e; } + } finally { + // Clear our reference to the runnable, which holds a reference + // to the transaction which, in turn, holds monitors. If we + // are run in the ModalContextThread and initialize an Xtext UI + // bundle, then that thread will be retained forever in its + // Guice injector as the creating thread, and that thread then + // will retain me (its runnable). See bug 498140 + privileged = null; } - }; - } + } + }; } diff --git a/tests/junit/plugins/core/org.eclipse.papyrus.infra.core.tests/test/org/eclipse/papyrus/infra/core/tests/AllTests.java b/tests/junit/plugins/core/org.eclipse.papyrus.infra.core.tests/test/org/eclipse/papyrus/infra/core/tests/AllTests.java index 063762cff96..9c4476c3738 100644 --- a/tests/junit/plugins/core/org.eclipse.papyrus.infra.core.tests/test/org/eclipse/papyrus/infra/core/tests/AllTests.java +++ b/tests/junit/plugins/core/org.eclipse.papyrus.infra.core.tests/test/org/eclipse/papyrus/infra/core/tests/AllTests.java @@ -10,7 +10,7 @@ * Remi Schnekenburger (CEA LIST) remi.schnekenburger@cea.fr - Initial API and implementation
* Christian W. Damus (CEA LIST) - add test for AdapterUtils
* Christian W. Damus (CEA) - bugs 402525, 422257, 399859
- * Christian W. Damus - bugs 456934, 468030, 482949, 485220, 488558
+ * Christian W. Damus - bugs 456934, 468030, 482949, 485220, 488558, 498140
*
*****************************************************************************/
package org.eclipse.papyrus.infra.core.tests;
@@ -27,6 +27,7 @@ import org.eclipse.papyrus.infra.core.services.SharedServiceFactoryTest; import org.eclipse.papyrus.infra.core.utils.AdapterUtilsTest;
import org.eclipse.papyrus.infra.core.utils.JobBasedFutureTest;
import org.eclipse.papyrus.infra.core.utils.JobExecutorServiceTest;
+import org.eclipse.papyrus.infra.core.utils.TransactionHelperTest;
import org.eclipse.papyrus.junit.framework.classification.ClassificationSuite;
import org.eclipse.papyrus.junit.framework.runner.Headless;
import org.junit.runner.RunWith;
@@ -45,7 +46,8 @@ import org.junit.runners.Suite.SuiteClasses; // {oep}.core.services
ComposedServiceTest.class, ServicesRegistryTest.class, SharedServiceFactoryTest.class,
// {oep}.core.utils
- AdapterUtilsTest.class, JobBasedFutureTest.class, JobExecutorServiceTest.class
+ AdapterUtilsTest.class, JobBasedFutureTest.class, JobExecutorServiceTest.class,
+ TransactionHelperTest.class,
})
/**
* Suite Class for all tests in the plugin
diff --git a/tests/junit/plugins/core/org.eclipse.papyrus.infra.core.tests/test/org/eclipse/papyrus/infra/core/utils/TransactionHelperTest.java b/tests/junit/plugins/core/org.eclipse.papyrus.infra.core.tests/test/org/eclipse/papyrus/infra/core/utils/TransactionHelperTest.java new file mode 100644 index 00000000000..6499866a806 --- /dev/null +++ b/tests/junit/plugins/core/org.eclipse.papyrus.infra.core.tests/test/org/eclipse/papyrus/infra/core/utils/TransactionHelperTest.java @@ -0,0 +1,194 @@ +/***************************************************************************** + * Copyright (c) 2016 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.infra.core.utils; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Consumer; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EcoreFactory; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.transaction.RecordingCommand; +import org.eclipse.emf.transaction.TransactionalEditingDomain; +import org.eclipse.papyrus.infra.tools.util.IProgressCallable; +import org.eclipse.papyrus.infra.tools.util.IProgressRunnable; +import org.eclipse.papyrus.junit.utils.PrintingProgressMonitor; +import org.eclipse.papyrus.junit.utils.rules.HouseKeeper; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +/** + * Automated tests for the {@link TransactionHelper} class. + */ +public class TransactionHelperTest { + + private static ExecutorService exec; + + @Rule + public final HouseKeeper houseKeeper = new HouseKeeper(); + + private TransactionalEditingDomain domain; + private EClass eclass; + + /** + * Initializes me. + */ + public TransactionHelperTest() { + super(); + } + + @Test + public void testPrivilegedProgressRunnable() { + IProgressRunnable runnable = monitor -> { + monitor.beginTask("Test privileged runnable", 2); + + eclass.setAbstract(true); + monitor.worked(1); + eclass.setName("Foo"); + monitor.worked(1); + + monitor.done(); + }; + + FailureAssertion failure = new FailureAssertion(); + domain.getCommandStack().execute(new RecordingCommand(domain, "Test") { + + @Override + protected void doExecute() { + IProgressRunnable privileged = TransactionHelper.createPrivilegedRunnable(domain, runnable); + Future<?> future = submit(privileged); + + // And synchronize with it + try { + future.get(); + } catch (Exception e) { + failure.accept(e); + } + } + }); + + failure.verify(); + + assertThat(eclass.isAbstract(), is(true)); + assertThat(eclass.getName(), is("Foo")); + } + + @Test + public void testPrivilegedProgressCallable() { + IProgressCallable<String> callable = monitor -> { + monitor.beginTask("Test privileged callable", 2); + + eclass.setAbstract(true); + monitor.worked(1); + eclass.setName("Foo"); + monitor.worked(1); + + monitor.done(); + + return eclass.getName(); + }; + + FailureAssertion failure = new FailureAssertion(); + domain.getCommandStack().execute(new RecordingCommand(domain, "Test") { + + @Override + protected void doExecute() { + IProgressCallable<String> privileged = TransactionHelper.createPrivilegedCallable(domain, callable); + Future<String> future = submit(privileged); + + // And synchronize with it + try { + String newName = future.get(); + assertThat(newName, is("Foo")); + } catch (Exception e) { + failure.accept(e); + } + } + }); + + failure.verify(); + + assertThat(eclass.isAbstract(), is(true)); + assertThat(eclass.getName(), is("Foo")); + } + + // + // Test framework + // + + @BeforeClass + public static void createExecutorService() { + exec = Executors.newSingleThreadExecutor(); + } + + @Before + public void createFixture() throws Exception { + domain = houseKeeper.createSimpleEditingDomain(); + Resource res = domain.createResource("file:bogus.ecore"); + + TransactionHelper.run(domain, () -> { + EPackage epackage = EcoreFactory.eINSTANCE.createEPackage(); + res.getContents().add(epackage); + eclass = EcoreFactory.eINSTANCE.createEClass(); + epackage.getEClassifiers().add(eclass); + }); + } + + @AfterClass + public static void shutdownExecutorService() { + exec.shutdown(); + exec = null; + } + + <V> Future<V> submit(IProgressCallable<V> callable) { + return exec.submit(() -> callable.call(new PrintingProgressMonitor())); + } + + Future<?> submit(IProgressRunnable runnable) { + return exec.submit(() -> runnable.run(new PrintingProgressMonitor())); + } + + // + // Nested types + // + + private static final class FailureAssertion implements Consumer<Throwable> { + private Throwable thrown; + + @Override + public void accept(Throwable t) { + // Take only the first one + if (thrown == null) { + thrown = t; + } + } + + void verify() { + if (thrown != null) { + thrown.printStackTrace(); + fail("Synchronization on future result failed: " + thrown.getMessage()); + } + } + } +} diff --git a/tests/junit/plugins/infra/org.eclipse.papyrus.infra.ui.tests/src/org/eclipse/papyrus/infra/ui/tests/AllTests.java b/tests/junit/plugins/infra/org.eclipse.papyrus.infra.ui.tests/src/org/eclipse/papyrus/infra/ui/tests/AllTests.java index 5d201a4bae4..3fd710a50e9 100644 --- a/tests/junit/plugins/infra/org.eclipse.papyrus.infra.ui.tests/src/org/eclipse/papyrus/infra/ui/tests/AllTests.java +++ b/tests/junit/plugins/infra/org.eclipse.papyrus.infra.ui.tests/src/org/eclipse/papyrus/infra/ui/tests/AllTests.java @@ -8,7 +8,7 @@ * * Contributors: * Christian W. Damus (CEA) - Initial API and implementation - * Christian W. Damus - bug 485220 + * Christian W. Damus - bugs 485220, 498140 * */ package org.eclipse.papyrus.infra.ui.tests; @@ -18,6 +18,7 @@ import org.eclipse.papyrus.infra.ui.lifecycleevents.LifeCycleEventsProviderTest; import org.eclipse.papyrus.infra.ui.lifecycleevents.SaveAndDirtyServiceTest; import org.eclipse.papyrus.infra.ui.providers.DelegatingPapyrusContentProviderTest; import org.eclipse.papyrus.infra.ui.providers.SemanticContentProviderFactoryTest; +import org.eclipse.papyrus.infra.ui.util.TransactionUIHelperTest; import org.eclipse.papyrus.infra.ui.util.UIUtilTest; import org.eclipse.papyrus.junit.framework.classification.ClassificationSuite; import org.junit.runner.RunWith; @@ -29,6 +30,7 @@ import org.junit.runners.Suite.SuiteClasses; */ @RunWith(ClassificationSuite.class) @SuiteClasses({ UIUtilTest.class, + TransactionUIHelperTest.class, SaveAndDirtyServiceTest.class, LifeCycleEventsProviderTest.class, NestedEditorDelegatedOutlinePageTest.class, diff --git a/tests/junit/plugins/infra/org.eclipse.papyrus.infra.ui.tests/src/org/eclipse/papyrus/infra/ui/util/TransactionUIHelperTest.java b/tests/junit/plugins/infra/org.eclipse.papyrus.infra.ui.tests/src/org/eclipse/papyrus/infra/ui/util/TransactionUIHelperTest.java new file mode 100644 index 00000000000..fb0b509d8a8 --- /dev/null +++ b/tests/junit/plugins/infra/org.eclipse.papyrus.infra.ui.tests/src/org/eclipse/papyrus/infra/ui/util/TransactionUIHelperTest.java @@ -0,0 +1,127 @@ +/***************************************************************************** + * Copyright (c) 2016 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.infra.ui.util; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +import java.util.function.Consumer; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EcoreFactory; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.transaction.RecordingCommand; +import org.eclipse.emf.transaction.TransactionalEditingDomain; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.papyrus.infra.core.utils.TransactionHelper; +import org.eclipse.papyrus.junit.utils.rules.HouseKeeper; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +/** + * Automated tests for the {@link TransactionUIHelper} class. + */ +public class TransactionUIHelperTest { + + @Rule + public final HouseKeeper houseKeeper = new HouseKeeper(); + + private TransactionalEditingDomain domain; + private EClass eclass; + + /** + * Initializes me. + */ + public TransactionUIHelperTest() { + super(); + } + + @Test + public void testPrivilegedRunnableWithProgress() { + IRunnableWithProgress runnable = monitor -> { + monitor.beginTask("Test privileged runnable", 2); + + eclass.setAbstract(true); + monitor.worked(1); + eclass.setName("Foo"); + monitor.worked(1); + + monitor.done(); + }; + + FailureAssertion failure = new FailureAssertion(); + domain.getCommandStack().execute(new RecordingCommand(domain, "Test") { + + @Override + protected void doExecute() { + IRunnableWithProgress privileged = TransactionUIHelper.createPrivilegedRunnableWithProgress(domain, runnable); + + try { + new ProgressMonitorDialog(null).run(true, true, privileged); + } catch (Exception e) { + failure.accept(e); + } + } + }); + + failure.verify(); + + assertThat(eclass.isAbstract(), is(true)); + assertThat(eclass.getName(), is("Foo")); + } + + // + // Test framework + // + + @Before + public void createFixture() throws Exception { + domain = houseKeeper.createSimpleEditingDomain(); + Resource res = domain.createResource("file:bogus.ecore"); + + TransactionHelper.run(domain, () -> { + EPackage epackage = EcoreFactory.eINSTANCE.createEPackage(); + res.getContents().add(epackage); + eclass = EcoreFactory.eINSTANCE.createEClass(); + epackage.getEClassifiers().add(eclass); + }); + } + + // + // Nested types + // + + private static final class FailureAssertion implements Consumer<Throwable> { + private Throwable thrown; + + @Override + public void accept(Throwable t) { + // Take only the first one + if (thrown == null) { + thrown = t; + } + } + + void verify() { + if (thrown != null) { + thrown.printStackTrace(); + fail("Synchronization on future result failed: " + thrown.getMessage()); + } + } + } +} |