/******************************************************************************* * Copyright (c) 2012, 2016 Matthias Sohn <matthias.sohn@sap.com> 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: * Thomas Wolf <thomas.wolf@paranor.ch> - Bugs 479964, 483664 *******************************************************************************/ package org.eclipse.egit.ui.view.repositories; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.File; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.util.Arrays; import java.util.List; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.egit.core.internal.indexdiff.IndexDiffCache; import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.JobFamilies; import org.eclipse.egit.ui.UIPreferences; import org.eclipse.egit.ui.internal.RepositoryCacheRule; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.egit.ui.test.ContextMenuHelper; import org.eclipse.egit.ui.test.TestUtil; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jgit.api.SubmoduleAddCommand; import org.eclipse.jgit.lib.Repository; import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner; import org.eclipse.swtbot.swt.finder.widgets.SWTBotCheckBox; import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTree; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** * SWTBot Tests for the Git Repositories View (repository deletion) */ @RunWith(SWTBotJunit4ClassRunner.class) public class GitRepositoriesViewRepoDeletionTest extends GitRepositoriesViewTestBase { private static final String DELETE_REPOSITORY_CONTEXT_MENU_LABEL = "RepoViewDeleteRepository.label"; private static final String REMOVE_REPOSITORY_FROM_VIEW_CONTEXT_MENU_LABEL = "RepoViewRemove.label"; private File repositoryFile; @Before public void before() throws Exception { repositoryFile = createProjectAndCommitToRepository(); } @Test public void testDeleteRepositoryWithContentOk() throws Exception { deleteAllProjects(); assertProjectExistence(PROJ1, false); clearView(); Activator.getDefault().getRepositoryUtil().addConfiguredRepository( repositoryFile); shareProjects(repositoryFile); assertProjectExistence(PROJ1, true); refreshAndWait(); assertHasRepo(repositoryFile); SWTBotTree tree = getOrOpenView().bot().tree(); tree.getAllItems()[0].select(); ContextMenuHelper.clickContextMenu(tree, myUtil .getPluginLocalizedValue(DELETE_REPOSITORY_CONTEXT_MENU_LABEL)); SWTBotShell shell = bot.shell(UIText.DeleteRepositoryConfirmDialog_DeleteRepositoryWindowTitle); shell.activate(); shell.bot() .checkBox( UIText.DeleteRepositoryConfirmDialog_DeleteGitDirCheckbox) .select(); shell.bot() .checkBox( UIText.DeleteRepositoryConfirmDialog_DeleteWorkingDirectoryCheckbox) .select(); shell.bot().button(IDialogConstants.OK_LABEL).click(); TestUtil.joinJobs(JobFamilies.REPOSITORY_DELETE); refreshAndWait(); assertEmpty(); assertProjectExistence(PROJ1, false); assertFalse(repositoryFile.exists()); assertFalse(new File(repositoryFile.getParentFile(), PROJ1).exists()); assertFalse(repositoryFile.getParentFile().exists()); } @Test public void testDeleteRepositoryKeepProjectsBug479964() throws Exception { deleteAllProjects(); assertProjectExistence(PROJ1, false); clearView(); Activator.getDefault().getRepositoryUtil() .addConfiguredRepository(repositoryFile); shareProjects(repositoryFile); assertProjectExistence(PROJ1, true); refreshAndWait(); assertHasRepo(repositoryFile); SWTBotTree tree = getOrOpenView().bot().tree(); tree.getAllItems()[0].select(); ContextMenuHelper.clickContextMenu(tree, myUtil .getPluginLocalizedValue(DELETE_REPOSITORY_CONTEXT_MENU_LABEL)); SWTBotShell shell = bot.shell( UIText.DeleteRepositoryConfirmDialog_DeleteRepositoryWindowTitle); shell.activate(); shell.bot() .checkBox( UIText.DeleteRepositoryConfirmDialog_DeleteGitDirCheckbox) .select(); SWTBotCheckBox checkbox = shell.bot().checkBox( UIText.DeleteRepositoryConfirmDialog_DeleteWorkingDirectoryCheckbox); checkbox.select(); checkbox.deselect(); // Now "Remove project from workspace" is selected, but "Delete working // tree" is not. shell.bot().button(IDialogConstants.OK_LABEL).click(); TestUtil.joinJobs(JobFamilies.REPOSITORY_DELETE); refreshAndWait(); assertEmpty(); assertProjectExistence(PROJ1, false); assertFalse(repositoryFile.exists()); assertTrue( new File(repositoryFile.getParentFile(), PROJ1).isDirectory()); } @Test public void testRemoveRepositoryRemoveFromCachesBug483664() throws Exception { Activator.getDefault().getPreferenceStore() .setValue(UIPreferences.ALWAYS_USE_STAGING_VIEW, false); deleteAllProjects(); assertProjectExistence(PROJ1, false); clearView(); refreshAndWait(); Activator.getDefault().getRepositoryUtil() .addConfiguredRepository(repositoryFile); refreshAndWait(); assertHasRepo(repositoryFile); SWTBotTree tree = getOrOpenView().bot().tree(); tree.getAllItems()[0].select(); ContextMenuHelper.clickContextMenuSync(tree, myUtil.getPluginLocalizedValue( REMOVE_REPOSITORY_FROM_VIEW_CONTEXT_MENU_LABEL)); TestUtil.joinJobs(JobFamilies.REPOSITORY_DELETE); refreshAndWait(); assertEmpty(); assertTrue(repositoryFile.exists()); assertTrue( new File(repositoryFile.getParentFile(), PROJ1).isDirectory()); TestUtil.waitForDecorations(); closeGitViews(); TestUtil.waitForJobs(500, 5000); // Session properties are stored in the Eclipse resource tree as part of // the resource info. org.eclipse.core.internal.dtree.DataTreeLookup has // a static LRU cache of lookup instances to avoid excessive strain on // the garbage collector due to constantly allocating and then // forgetting instances. These lookup objects may contain things // recently queried or modified in the resource tree, such as session // properties. As a result, the session properties of a deleted resource // remain around a little longer than expected: to be precise, exactly // 100 more queries on the Eclipse resource tree until the entry // containing the session property is recycled. We use session // properties to store the RepositoryMappings, which reference the // repository. // // Make sure we clear that cache: IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IProject project = root.getProject(PROJ1); for (int i = 0; i < 101; i++) { // Number of iterations at least DataTreeLookup.POOL_SIZE! // Use up one DataTreeLookup instance: project.create(null); if (i == 0) { // Furthermore, the WorkbenchSourceProvider has still a // reference to the last selection, which is our now long // removed repository node! Arguably that's a strange thing, but // strictly speaking, since there is no guarantee _when_ a // weakly referenced object is removed, not even making // WorkbenchSourceProvider.lastShowInSelection a WeakReference // might help. Therefore, let's make sure that the last "show // in" selection is no longer the RepositoryNode, which also // still has a reference to the repository. That last "show in" // selection is set when the "Shown in..." context menu is // filled, which happens when the project explorer's context // menu is activated. So we have to open that menu at least once // with a different selection. SWTBotTree explorerTree = TestUtil.getExplorerTree(); SWTBotTreeItem projectNode = TestUtil.navigateTo(explorerTree, PROJ1); projectNode.select(); ContextMenuHelper.isContextMenuItemEnabled(explorerTree, "New"); } project.delete(true, true, null); } TestUtil.waitForJobs(500, 5000); // And we may have the RepositoryChangeScanner running: use a job // with a scheduling rule that ensures we have exclusive access. final String[] results = { null, null }; Job verifier = new Job(testName.getMethodName()) { @Override protected IStatus run(IProgressMonitor monitor) { // Wait for things to definitely quieten down. Note that // waitForJobs only waits for running and waiting jobs, there // may still be scheduled jobs that might wake up and run after // that. TestUtil.joinJobs does really join, which also waits // for scheduled jobs. try { TestUtil.joinJobs( org.eclipse.egit.core.JobFamilies.INDEX_DIFF_CACHE_UPDATE); // Is this job doing something when the view is hidden? TestUtil.joinJobs(JobFamilies.REPO_VIEW_REFRESH); TestUtil.waitForDecorations(); } catch (InterruptedException e) { results[0] = "Interrupted"; Thread.currentThread().interrupt(); return Status.CANCEL_STATUS; } // Finally... Java does not give any guarantees about when // exactly an only weakly reachable object is finalized and // garbage collected. waitForFinalization(5000); // Experience shows that an explicit garbage collection run very // often does reclaim only weakly reachable objects and set the // weak references' referents to null, but not even that can be // guaranteed! Whether or not it does may also depend on the // configuration of the JVM (such as through command-line // arguments). Repository[] repositories = org.eclipse.egit.core.Activator .getDefault().getRepositoryCache().getAllRepositories(); results[0] = Arrays.asList(repositories).toString(); IndexDiffCache indexDiffCache = org.eclipse.egit.core.Activator .getDefault().getIndexDiffCache(); results[1] = indexDiffCache.currentCacheEntries().toString(); return Status.OK_STATUS; } }; verifier.setRule(new RepositoryCacheRule()); verifier.setSystem(true); verifier.schedule(); verifier.join(); List<String> configuredRepos = org.eclipse.egit.core.Activator .getDefault().getRepositoryUtil().getConfiguredRepositories(); assertTrue("Expected no configured repositories", configuredRepos.isEmpty()); assertEquals("Expected no cached repositories", "[]", results[0]); assertEquals("Expected no IndexDiffCache entries", "[]", results[1]); Activator.getDefault().getPreferenceStore() .setValue(UIPreferences.ALWAYS_USE_STAGING_VIEW, true); } @Test public void testDeleteSubmoduleRepository() throws Exception { deleteAllProjects(); assertProjectExistence(PROJ1, false); clearView(); Activator.getDefault().getRepositoryUtil() .addConfiguredRepository(repositoryFile); shareProjects(repositoryFile); assertProjectExistence(PROJ1, true); refreshAndWait(); assertHasRepo(repositoryFile); Repository db = lookupRepository(repositoryFile); SubmoduleAddCommand command = new SubmoduleAddCommand(db); String path = "sub"; command.setPath(path); String uri = db.getDirectory().toURI().toString(); command.setURI(uri); Repository subRepo = command.call(); assertNotNull(subRepo); subRepo.close(); refreshAndWait(); SWTBotTree tree = getOrOpenView().bot().tree(); SWTBotTreeItem item = TestUtil.expandAndWait(tree.getAllItems()[0]); item = TestUtil.expandAndWait(item.getNode( UIText.RepositoriesViewLabelProvider_SubmodulesNodeText)); item.getItems()[0].select(); ContextMenuHelper.clickContextMenu(tree, myUtil .getPluginLocalizedValue(DELETE_REPOSITORY_CONTEXT_MENU_LABEL)); SWTBotShell shell = bot .shell(UIText.DeleteRepositoryConfirmDialog_DeleteRepositoryWindowTitle); shell.activate(); shell.bot() .checkBox( UIText.DeleteRepositoryConfirmDialog_DeleteGitDirCheckbox) .select(); shell.bot() .checkBox( UIText.DeleteRepositoryConfirmDialog_DeleteWorkingDirectoryCheckbox) .select(); shell.bot().button(IDialogConstants.OK_LABEL).click(); TestUtil.joinJobs(JobFamilies.REPOSITORY_DELETE); refreshAndWait(); assertFalse(subRepo.getDirectory().exists()); assertFalse(subRepo.getWorkTree().exists()); } /** * Best-effort attempt to get finalization to occur. * * @param maxMillis * maximum amount of time in milliseconds to try getting the * garbage collector to finalize objects */ private void waitForFinalization(int maxMillis) { long stop = System.currentTimeMillis() + maxMillis; MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); do { System.gc(); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } while (System.currentTimeMillis() < stop && memoryBean.getObjectPendingFinalizationCount() > 0); } }