Bug 552852 - [Categorization] ViewModelContext leaks in tab renderings

- ensure that the renderer used to render the contents of a tab is finalized so that it will hook its dispose listener to remove itself as a view model context user
- enhance existing unit tests with assertion of view model context user clean-up

Change-Id: I278ac8043a676a63d2218ccb098fff408f42efae
Signed-off-by: Christian W. Damus <give.a.damus@gmail.com>
diff --git a/bundles/org.eclipse.emf.ecp.view.categorization.swt/src/org/eclipse/emf/ecp/view/spi/categorization/swt/AbstractSWTTabRenderer.java b/bundles/org.eclipse.emf.ecp.view.categorization.swt/src/org/eclipse/emf/ecp/view/spi/categorization/swt/AbstractSWTTabRenderer.java
index cea82c1..b765c6a 100644
--- a/bundles/org.eclipse.emf.ecp.view.categorization.swt/src/org/eclipse/emf/ecp/view/spi/categorization/swt/AbstractSWTTabRenderer.java
+++ b/bundles/org.eclipse.emf.ecp.view.categorization.swt/src/org/eclipse/emf/ecp/view/spi/categorization/swt/AbstractSWTTabRenderer.java
@@ -10,7 +10,7 @@
  *
  * Contributors:
  * Eugen Neufeld - initial API and implementation
- * Christian W. Damus - bug 548592
+ * Christian W. Damus - bugs 548592, 552852
  ******************************************************************************/
 package org.eclipse.emf.ecp.view.spi.categorization.swt;
 
@@ -200,6 +200,7 @@
 			.createEmptyGridDescription());
 		for (final SWTGridCell gridCell : gridDescription.getGrid()) {
 			final Control render = renderer.render(gridCell, composite);
+			renderer.finalizeRendering(composite);
 			GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true)
 				.applyTo(render);
 			if (useScrolledContent) {
diff --git a/tests/org.eclipse.emf.ecp.view.categorization.swt.test/CategorizationTests.launch b/tests/org.eclipse.emf.ecp.view.categorization.swt.test/CategorizationTests.launch
index 1a5edf0..3701fe3 100644
--- a/tests/org.eclipse.emf.ecp.view.categorization.swt.test/CategorizationTests.launch
+++ b/tests/org.eclipse.emf.ecp.view.categorization.swt.test/CategorizationTests.launch
@@ -33,7 +33,7 @@
 <stringAttribute key="pde.version" value="3.3"/>
 <stringAttribute key="product" value="org.eclipse.sdk.ide"/>
 <booleanAttribute key="run_in_ui_thread" value="true"/>
-<stringAttribute key="selected_target_plugins" value="com.ibm.icu@default:default,javax.annotation@default:default,javax.inject@default:default,org.apache.batik.constants*1.11.0.v20190515-0436@default:default,org.apache.batik.css*1.11.0.v20190515-0436@default:default,org.apache.batik.i18n*1.11.0.v20190515-0436@default:default,org.apache.batik.util*1.11.0.v20190515-0436@default:default,org.apache.commons.codec@default:default,org.apache.commons.jxpath@default:default,org.apache.commons.logging*1.1.1.v201101211721@default:default,org.apache.commons.logging*1.2.0.v20180409-1502@default:default,org.apache.felix.scr@1:true,org.apache.xmlgraphics@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.resources@default:default,org.eclipse.core.runtime@default:true,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.annotations@default:default,org.eclipse.e4.core.di.extensions.supplier@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.e4.emf.xpath@default:default,org.eclipse.e4.ui.bindings@default:default,org.eclipse.e4.ui.css.core@default:default,org.eclipse.e4.ui.css.swt.theme@default:default,org.eclipse.e4.ui.css.swt@default:default,org.eclipse.e4.ui.di@default:default,org.eclipse.e4.ui.model.workbench@default:default,org.eclipse.e4.ui.services@default:default,org.eclipse.e4.ui.widgets@default:default,org.eclipse.e4.ui.workbench.addons.swt@default:default,org.eclipse.e4.ui.workbench.renderers.swt@default:default,org.eclipse.e4.ui.workbench.swt@default:default,org.eclipse.e4.ui.workbench3@default:default,org.eclipse.e4.ui.workbench@default:default,org.eclipse.emf.common.ui@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.databinding.edit@default:default,org.eclipse.emf.databinding@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.edit@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.emf.edit.ui@default:default,org.eclipse.emf.edit@default:default,org.eclipse.emf.emfstore.client@default:default,org.eclipse.emf.emfstore.common.model@default:default,org.eclipse.emf.emfstore.common@default:default,org.eclipse.emf.emfstore.examplemodel.edit@default:default,org.eclipse.emf.emfstore.examplemodel@default:default,org.eclipse.emf.emfstore.migration@default:default,org.eclipse.emf.emfstore.server.model@default:default,org.eclipse.emf.emfstore.server@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.ds@1:true,org.eclipse.equinox.event@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.util@default:default,org.eclipse.help@default:default,org.eclipse.jface.databinding@default:default,org.eclipse.jface.text@default:default,org.eclipse.jface@default:default,org.eclipse.net4j.util@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi.util@default:default,org.eclipse.osgi@-1:true,org.eclipse.swt.cocoa.macosx.x86_64@default:false,org.eclipse.swt@default:default,org.eclipse.text@default:default,org.eclipse.ui.forms@default:default,org.eclipse.ui.trace@default:default,org.eclipse.ui.views@default:default,org.eclipse.ui.workbench@default:default,org.eclipse.ui@default:default,org.hamcrest.core@default:default,org.junit@default:default,org.mockito.mockito-core-hamcrest-modified@default:default,org.objenesis@default:default,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil*1.0.0.v200806040011@default:default,org.w3c.dom.smil*1.0.1.v200903091627@default:default,org.w3c.dom.svg@default:default"/>
+<stringAttribute key="selected_target_plugins" value="com.ibm.icu@default:default,javax.annotation@default:default,javax.inject@default:default,org.apache.batik.constants*1.11.0.v20190515-0436@default:default,org.apache.batik.css*1.11.0.v20190515-0436@default:default,org.apache.batik.i18n*1.11.0.v20190515-0436@default:default,org.apache.batik.util*1.11.0.v20190515-0436@default:default,org.apache.commons.codec@default:default,org.apache.commons.jxpath@default:default,org.apache.commons.logging*1.1.1.v201101211721@default:default,org.apache.commons.logging*1.2.0.v20180409-1502@default:default,org.apache.felix.scr@1:true,org.apache.xmlgraphics@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.resources@default:default,org.eclipse.core.runtime@default:true,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.annotations@default:default,org.eclipse.e4.core.di.extensions.supplier@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.e4.emf.xpath@default:default,org.eclipse.e4.ui.bindings@default:default,org.eclipse.e4.ui.css.core@default:default,org.eclipse.e4.ui.css.swt.theme@default:default,org.eclipse.e4.ui.css.swt@default:default,org.eclipse.e4.ui.di@default:default,org.eclipse.e4.ui.dialogs@default:default,org.eclipse.e4.ui.model.workbench@default:default,org.eclipse.e4.ui.services@default:default,org.eclipse.e4.ui.widgets@default:default,org.eclipse.e4.ui.workbench.addons.swt@default:default,org.eclipse.e4.ui.workbench.renderers.swt@default:default,org.eclipse.e4.ui.workbench.swt@default:default,org.eclipse.e4.ui.workbench3@default:default,org.eclipse.e4.ui.workbench@default:default,org.eclipse.emf.common.ui@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.databinding.edit@default:default,org.eclipse.emf.databinding@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.edit@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.emf.edit.ui@default:default,org.eclipse.emf.edit@default:default,org.eclipse.emf.emfstore.client@default:default,org.eclipse.emf.emfstore.common.model@default:default,org.eclipse.emf.emfstore.common@default:default,org.eclipse.emf.emfstore.examplemodel.edit@default:default,org.eclipse.emf.emfstore.examplemodel@default:default,org.eclipse.emf.emfstore.migration@default:default,org.eclipse.emf.emfstore.server.model@default:default,org.eclipse.emf.emfstore.server@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.ds@1:true,org.eclipse.equinox.event@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.util@default:default,org.eclipse.help@default:default,org.eclipse.jface.databinding@default:default,org.eclipse.jface.text@default:default,org.eclipse.jface@default:default,org.eclipse.net4j.util@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi.util@default:default,org.eclipse.osgi@-1:true,org.eclipse.swt.cocoa.macosx.x86_64@default:false,org.eclipse.swt@default:default,org.eclipse.text@default:default,org.eclipse.ui.forms@default:default,org.eclipse.ui.trace@default:default,org.eclipse.ui.views@default:default,org.eclipse.ui.workbench@default:default,org.eclipse.ui@default:default,org.hamcrest.core@default:default,org.junit@default:default,org.mockito.mockito-core-hamcrest-modified@default:default,org.objenesis@default:default,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil*1.0.0.v200806040011@default:default,org.w3c.dom.smil*1.0.1.v200903091627@default:default,org.w3c.dom.svg@default:default"/>
 <stringAttribute key="selected_workspace_plugins" value="org.eclipse.emf.ecp.common.ui@default:default,org.eclipse.emf.ecp.common@default:default,org.eclipse.emf.ecp.core@default:default,org.eclipse.emf.ecp.edit.swt@default:default,org.eclipse.emf.ecp.edit@default:default,org.eclipse.emf.ecp.editor.e3@default:default,org.eclipse.emf.ecp.emfstore.core@default:default,org.eclipse.emf.ecp.explorereditorbridge@default:default,org.eclipse.emf.ecp.makeithappen.model@default:default,org.eclipse.emf.ecp.test.common@default:default,org.eclipse.emf.ecp.test.model@default:default,org.eclipse.emf.ecp.ui.view.swt@default:default,org.eclipse.emf.ecp.ui.view.test@default:default,org.eclipse.emf.ecp.ui.view@default:default,org.eclipse.emf.ecp.ui@default:default,org.eclipse.emf.ecp.view.categorization.model.edit@default:default,org.eclipse.emf.ecp.view.categorization.model@default:default,org.eclipse.emf.ecp.view.categorization.swt.test@default:false,org.eclipse.emf.ecp.view.categorization.swt@default:default,org.eclipse.emf.ecp.view.context@default:default,org.eclipse.emf.ecp.view.core.swt@default:default,org.eclipse.emf.ecp.view.horizontal.model@default:default,org.eclipse.emf.ecp.view.label.model@default:default,org.eclipse.emf.ecp.view.migrator@default:default,org.eclipse.emf.ecp.view.model.common.di@default:default,org.eclipse.emf.ecp.view.model.common@default:default,org.eclipse.emf.ecp.view.model.edit@default:default,org.eclipse.emf.ecp.view.model.provider.generator@default:default,org.eclipse.emf.ecp.view.model.provider.xmi@default:default,org.eclipse.emf.ecp.view.model@default:default,org.eclipse.emf.ecp.view.rule.model@default:default,org.eclipse.emf.ecp.view.rule@default:default,org.eclipse.emf.ecp.view.swt.layout@default:default,org.eclipse.emf.ecp.view.table.model@default:default,org.eclipse.emf.ecp.view.template.model@default:default,org.eclipse.emf.ecp.view.template.service@default:default,org.eclipse.emf.ecp.view.test.common.swt@default:default,org.eclipse.emf.ecp.view.test.common@default:default,org.eclipse.emf.ecp.view.util.swt@default:default,org.eclipse.emf.ecp.view.validation@default:default,org.eclipse.emf.ecp.view.vertical.model@default:default,org.eclipse.emfforms.common.validation@default:default,org.eclipse.emfforms.common@default:default,org.eclipse.emfforms.core.bazaar@default:default,org.eclipse.emfforms.core.services.databinding.featurepath@default:default,org.eclipse.emfforms.core.services.databinding.testmodel@default:default,org.eclipse.emfforms.core.services.domainexpander.default@default:default,org.eclipse.emfforms.core.services.editsupport@default:default,org.eclipse.emfforms.core.services.emf@default:default,org.eclipse.emfforms.core.services.emfspecificservice@default:default,org.eclipse.emfforms.core.services.legacy@default:default,org.eclipse.emfforms.core.services.locale.default@default:default,org.eclipse.emfforms.core.services.mappingprovider.default@default:default,org.eclipse.emfforms.core.services.segments@default:default,org.eclipse.emfforms.core.services.structuralchange.default@default:default,org.eclipse.emfforms.core.services.structuralchange@default:default,org.eclipse.emfforms.core.services@default:default,org.eclipse.emfforms.localization@default:default,org.eclipse.emfforms.swt.core.di.extension@default:default,org.eclipse.emfforms.swt.core.di@default:default,org.eclipse.emfforms.swt.core@default:default,org.eclipse.emfforms.view.annotation.model@default:default,org.eclipse.emfforms.view.model.localization@default:default,org.eclipse.emfforms.view.multisegment.model@default:default"/>
 <booleanAttribute key="show_selected_only" value="false"/>
 <booleanAttribute key="tracing" value="false"/>
diff --git a/tests/org.eclipse.emf.ecp.view.categorization.swt.test/src/org/eclipse/emf/ecp/view/spi/categorization/swt/CategorizationRenderer_PTest.java b/tests/org.eclipse.emf.ecp.view.categorization.swt.test/src/org/eclipse/emf/ecp/view/spi/categorization/swt/CategorizationRenderer_PTest.java
index c7f6127..ae08028 100644
--- a/tests/org.eclipse.emf.ecp.view.categorization.swt.test/src/org/eclipse/emf/ecp/view/spi/categorization/swt/CategorizationRenderer_PTest.java
+++ b/tests/org.eclipse.emf.ecp.view.categorization.swt.test/src/org/eclipse/emf/ecp/view/spi/categorization/swt/CategorizationRenderer_PTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2011-2014 EclipseSource Muenchen GmbH and others.
+ * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others.
  *
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -10,14 +10,28 @@
  *
  * Contributors:
  * Eugen - initial API and implementation
+ * Christian W. Damus - bug 552852
  ******************************************************************************/
 package org.eclipse.emf.ecp.view.spi.categorization.swt;
 
+import static java.util.Collections.singleton;
+import static org.eclipse.emf.ecp.view.test.common.spi.EMFMocking.eMock;
+import static org.hamcrest.CoreMatchers.anything;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.util.HashSet;
+import java.util.Set;
+
 import org.eclipse.emf.common.util.BasicEList;
 import org.eclipse.emf.common.util.EList;
 import org.eclipse.emf.common.util.TreeIterator;
@@ -42,6 +56,7 @@
 import org.eclipse.emf.emfstore.bowling.BowlingFactory;
 import org.eclipse.emf.emfstore.bowling.Player;
 import org.eclipse.emfforms.spi.common.report.ReportService;
+import org.eclipse.emfforms.spi.swt.core.EMFFormsNoRendererException;
 import org.eclipse.emfforms.spi.swt.core.EMFFormsRendererFactory;
 import org.eclipse.emfforms.spi.swt.core.layout.SWTGridCell;
 import org.eclipse.swt.custom.CTabFolder;
@@ -52,7 +67,6 @@
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.Tree;
 import org.eclipse.swt.widgets.TreeItem;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -63,13 +77,6 @@
 @RunWith(DatabindingClassRunner.class)
 public class CategorizationRenderer_PTest {
 
-	/**
-	 * @throws java.lang.Exception
-	 */
-	@Before
-	public void setUp() throws Exception {
-	}
-
 	@Test
 	public void testCategorizationElementTreeRenderer() throws NoRendererFoundException,
 		NoPropertyDescriptorFoundExeption {
@@ -98,22 +105,30 @@
 
 	@Test
 	public void testCategorizationElementTabRenderer() throws NoRendererFoundException,
-		NoPropertyDescriptorFoundExeption {
+		NoPropertyDescriptorFoundExeption, EMFFormsNoRendererException {
+
+		final Set<Object> contextUsers = new HashSet<>();
+
 		final ReportService reportService = mock(ReportService.class);
-		final EMFFormsRendererFactory emfFormsRendererFactory = mock(EMFFormsRendererFactory.class);
+		final EMFFormsRendererFactory emfFormsRendererFactory = mockRendererFactory(reportService);
 
 		final SWTGridCell gridCell = mock(SWTGridCell.class);
 		final Shell shell = new Shell();
 		final VCategorizationElement categorizationElement = mock(VCategorizationElement.class);
 		final EList<VAbstractCategorization> categorizations = new BasicEList<VAbstractCategorization>();
+		categorizations.add(eMock(VCategory.class));
 		when(categorizationElement.getCategorizations()).thenReturn(categorizations);
-		final ViewModelContext vmc = mock(ViewModelContext.class);
+		final ViewModelContext vmc = mockViewModelContext(contextUsers);
 		final VTViewTemplateProvider viewTemplateProvider = mock(VTViewTemplateProvider.class);
 		final CategorizationElementTabbedSWTRenderer categorizatrionElementRenderer = new CategorizationElementTabbedSWTRenderer(
 			categorizationElement, vmc, reportService, emfFormsRendererFactory, viewTemplateProvider);
 		categorizatrionElementRenderer.init();
 		final Control render = categorizatrionElementRenderer.render(gridCell, shell);
+		categorizatrionElementRenderer.finalizeRendering(shell);
 		assertTrue(CTabFolder.class.isInstance(render));
+
+		shell.dispose();
+		assertThat("Some context user remains", contextUsers, not(hasItem(anything())));
 	}
 
 	@Test
@@ -190,8 +205,11 @@
 	@Test
 	public void testCompositeCategoryElementTabRenderer() throws NoRendererFoundException,
 		NoPropertyDescriptorFoundExeption {
+
+		final Set<Object> contextUsers = new HashSet<>();
+
 		final ReportService reportService = mock(ReportService.class);
-		final EMFFormsRendererFactory emfFormsRendererFactory = mock(EMFFormsRendererFactory.class);
+		final EMFFormsRendererFactory emfFormsRendererFactory = mockRendererFactory(reportService);
 
 		final SWTGridCell gridCell = mock(SWTGridCell.class);
 		final Shell shell = new Shell();
@@ -199,15 +217,21 @@
 		final EList<VAbstractCategorization> categorizations = new BasicEList<VAbstractCategorization>();
 		final VCategorization categorization = mock(VCategorization.class);
 		categorizations.add(categorization);
-		when(categorization.getCategorizations()).thenReturn(new BasicEList<VAbstractCategorization>());
+		final EList<VAbstractCategorization> tabs = new BasicEList<VAbstractCategorization>(
+			singleton(eMock(VCategory.class)));
+		when(categorization.getCategorizations()).thenReturn(tabs);
 		when(categorizationElement.getCategorizations()).thenReturn(categorizations);
-		final ViewModelContext vmc = mock(ViewModelContext.class);
+		final ViewModelContext vmc = mockViewModelContext(contextUsers);
 		final VTViewTemplateProvider viewTemplateProvider = mock(VTViewTemplateProvider.class);
 		final CompositeCategorySWTTabRenderer categorizatrionElementRenderer = new CompositeCategorySWTTabRenderer(
 			categorization, vmc, reportService, emfFormsRendererFactory, viewTemplateProvider);
 		categorizatrionElementRenderer.init();
 		final Control render = categorizatrionElementRenderer.render(gridCell, shell);
+		categorizatrionElementRenderer.finalizeRendering(shell);
 		assertTrue(CTabFolder.class.isInstance(render));
+
+		shell.dispose();
+		assertThat("Some context user remains", contextUsers, not(hasItem(anything())));
 	}
 
 	@Test
@@ -276,4 +300,34 @@
 		assertTrue(tree.isEnabled());
 		assertTrue(detailComposite.isEnabled());
 	}
+
+	//
+	// Test framework
+	//
+
+	ViewModelContext mockViewModelContext(Set<Object> users) {
+		final ViewModelContext result = mock(ViewModelContext.class);
+		doAnswer(invocation -> users.add(invocation.getArguments()[0])).when(result).addContextUser(any());
+		doAnswer(invocation -> users.remove(invocation.getArguments()[0])).when(result).removeContextUser(any());
+		return result;
+	}
+
+	EMFFormsRendererFactory mockRendererFactory(ReportService reportService) {
+		final EMFFormsRendererFactory result = mock(EMFFormsRendererFactory.class);
+		try {
+			when(result.getRendererInstance(isA(VCategory.class), any()))
+				.thenAnswer(invocation -> {
+					final SWTCategoryRenderer renderer = new SWTCategoryRenderer(
+						(VCategory) invocation.getArguments()[0],
+						(ViewModelContext) invocation.getArguments()[1], reportService, result);
+					renderer.init();
+					return renderer;
+				});
+		} catch (final EMFFormsNoRendererException e) {
+			e.printStackTrace();
+			fail("Exception in mock set-up: " + e.getMessage());
+		}
+
+		return result;
+	}
 }