diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java index e1d3fff3a8e..16dbdeccafb 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java @@ -1115,6 +1115,12 @@ protected boolean refreshResource(IResource target, int depth, boolean updateAli SubMonitor refreshMonitor = subMonitor.newChild(98); RefreshLocalVisitor visitor = updateAliases ? new RefreshLocalAliasVisitor(refreshMonitor) : new RefreshLocalVisitor(refreshMonitor); tree.accept(visitor, depth); + // a project without description file is closed rather than recreating the file + for (Project project : visitor.getProjectsWithoutDescription()) { + if (project.isOpen()) { + project.basicClose(null); + } + } IStatus result = visitor.getErrorStatus(); if (!result.isOK()) { throw new ResourceException(result); @@ -1487,42 +1493,6 @@ public void write(IFolder target, boolean force, IProgressMonitor monitor) throw updateLocalSync(info, store.fetchInfo().getLastModified()); } - /** - * Write the .project file without modifying the resource tree. This is called - * during save when it is discovered that the .project file is missing. The tree - * cannot be modified during save. - */ - public void writeSilently(IProject target) throws CoreException { - IPath location = locationFor(target, false); - //if the project location cannot be resolved, we don't know if a description file exists or not - if (location == null) { - return; - } - IFileStore projectStore = getStore(target); - projectStore.mkdir(EFS.NONE, null); - //can't do anything if there's no description - IProjectDescription desc = ((Project) target).internalGetDescription(); - if (desc == null) { - return; - } - //write the project's private description to the meta-data area - getWorkspace().getMetaArea().writePrivateDescription(target); - - //write the file that represents the project description - IFileStore fileStore = projectStore.getChild(IProjectDescription.DESCRIPTION_FILE_NAME); - try ( - OutputStream out = fileStore.openOutputStream(EFS.NONE, null) - ) { - IFile file = target.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); - new ModelObjectWriter().write(desc, out, file.getLineSeparator(true)); - } catch (IOException e) { - String msg = NLS.bind(Messages.resources_writeMeta, target.getFullPath()); - throw new ResourceException(IResourceStatus.FAILED_WRITE_METADATA, target.getFullPath(), msg, e); - } - //for backwards compatibility, ensure the old .prj file is deleted - getWorkspace().getMetaArea().clearOldDescription(target); - } - public boolean storeHistory(IResource file) { WorkspaceDescription description = workspace.internalGetDescription(); return (description.isKeepDerivedState() || !file.isDerived()) && !disableHistory(file); diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/RefreshLocalVisitor.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/RefreshLocalVisitor.java index ee136683f1c..78d721a7899 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/RefreshLocalVisitor.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/RefreshLocalVisitor.java @@ -14,10 +14,13 @@ *******************************************************************************/ package org.eclipse.core.internal.localstore; +import java.util.LinkedHashSet; +import java.util.Set; import org.eclipse.core.internal.resources.Container; import org.eclipse.core.internal.resources.File; import org.eclipse.core.internal.resources.Folder; import org.eclipse.core.internal.resources.ICoreConstants; +import org.eclipse.core.internal.resources.Project; import org.eclipse.core.internal.resources.Resource; import org.eclipse.core.internal.resources.ResourceInfo; import org.eclipse.core.internal.resources.ResourceStatus; @@ -56,6 +59,7 @@ public class RefreshLocalVisitor implements IUnifiedTreeVisitor, ILocalStoreCons protected SubMonitor monitor; protected boolean resourceChanged; protected Workspace workspace; + private final Set projectsWithoutDescription = new LinkedHashSet<>(); public RefreshLocalVisitor(IProgressMonitor monitor) { this.monitor = SubMonitor.convert(monitor); @@ -146,6 +150,13 @@ protected void folderToFile(UnifiedTreeNode node, Resource target) throws CoreEx target.getLocalManager().updateLocalSync(info, node.getLastModified()); } + /** + * Returns the open projects whose description file was found deleted. + */ + public Set getProjectsWithoutDescription() { + return projectsWithoutDescription; + } + /** * Returns the status of the nodes visited so far. This will be a multi-status * that describes all problems that have occurred, or an OK status if everything @@ -314,13 +325,22 @@ public boolean visit(UnifiedTreeNode node) throws CoreException { errors.merge(new ResourceStatus(IResourceStatus.INVALID_RESOURCE_NAME, message)); return false; } + boolean existedInWorkspace = node.existsInWorkspace(); int state = synchronizeExistence(node, target); if (state == RL_IN_SYNC || state == RL_NOT_IN_SYNC) { if (targetType == IResource.FILE) { - try { - ((File) target).updateMetadataFiles(); - } catch (CoreException e) { - errors.merge(e.getStatus()); + File file = (File) target; + Project project = (Project) file.getProject(); + // a filtered description file is still read from disk + if (existedInWorkspace && !file.exists() && file.isProjectDescriptionFile() + && !file.getLocalManager().hasSavedDescription(project)) { + projectsWithoutDescription.add(project); + } else { + try { + file.updateMetadataFiles(); + } catch (CoreException e) { + errors.merge(e.getStatus()); + } } } return true; diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/File.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/File.java index eff57d98b85..f1227ef42a9 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/File.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/File.java @@ -557,7 +557,7 @@ public void setContents(byte[] content, int updateFlags, IProgressMonitor monito public long setLocalTimeStamp(long value) throws CoreException { //override to handle changing timestamp on project description file long result = super.setLocalTimeStamp(value); - if (path.segmentCount() == 2 && path.segment(1).equals(IProjectDescription.DESCRIPTION_FILE_NAME)) { + if (isProjectDescriptionFile()) { //handle concurrent project deletion ResourceInfo projectInfo = ((Project) getProject()).getResourceInfo(false, false); if (projectInfo != null) { @@ -576,10 +576,7 @@ public long setLocalTimeStamp(long value) throws CoreException { * been modified (added, removed, or changed). */ public void updateMetadataFiles() throws CoreException { - int count = path.segmentCount(); - String name = path.segment(1); - // is this a project description file? - if (count == 2 && name.equals(IProjectDescription.DESCRIPTION_FILE_NAME)) { + if (isProjectDescriptionFile()) { Project project = (Project) getProject(); project.updateDescription(); // Discard stale project natures on ProjectInfo @@ -588,12 +585,19 @@ public void updateMetadataFiles() throws CoreException { return; } // check to see if we are in the .settings directory - if (count == 3 && EclipsePreferences.DEFAULT_PREFERENCES_DIRNAME.equals(name)) { + if (path.segmentCount() == 3 && EclipsePreferences.DEFAULT_PREFERENCES_DIRNAME.equals(path.segment(1))) { ProjectPreferences.updatePreferences(this); return; } } + /** + * Returns whether this file is the description file (.project) of its project. + */ + public boolean isProjectDescriptionFile() { + return path.segmentCount() == 2 && path.segment(1).equals(IProjectDescription.DESCRIPTION_FILE_NAME); + } + @Deprecated @Override public void setCharset(String newCharset) throws CoreException { diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java index 47413ea6c2c..9529a25e887 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java @@ -235,19 +235,8 @@ public void close(IProgressMonitor monitor) throws CoreException { if (!isOpen(flags)) { return; } - // Signal that this resource is about to be closed. Do this at the very - // beginning so that infrastructure pieces have a chance to do clean up - // while the resources still exist. workspace.beginOperation(true); - workspace.broadcastEvent(LifecycleEvent.newEvent(LifecycleEvent.PRE_PROJECT_CLOSE, this)); - // flush the build order early in case there is a problem - workspace.flushBuildOrder(); - IProgressMonitor sub = subMonitor.newChild(49, SubMonitor.SUPPRESS_SUBTASK); - IStatus saveStatus = workspace.getSaveManager().save(ISaveContext.PROJECT_SAVE, this, sub); - internalClose(subMonitor.newChild(49)); - if (saveStatus != null && !saveStatus.isOK()) { - throw new ResourceException(saveStatus); - } + basicClose(subMonitor.newChild(98)); } catch (OperationCanceledException e) { workspace.getWorkManager().operationCanceled(); throw e; @@ -670,6 +659,26 @@ private boolean shouldBuild() { workspace.run(buildRunnable, null, IWorkspace.AVOID_UPDATE, monitor); } + /** + * Closes this open project. Must be called from within a workspace operation + * whose scheduling rule covers this project. + */ + public void basicClose(IProgressMonitor monitor) throws CoreException { + SubMonitor subMonitor = SubMonitor.convert(monitor, 2); + // Signal that this resource is about to be closed. Do this at the very + // beginning so that infrastructure pieces have a chance to do clean up + // while the resources still exist. + workspace.broadcastEvent(LifecycleEvent.newEvent(LifecycleEvent.PRE_PROJECT_CLOSE, this)); + // flush the build order early in case there is a problem + workspace.flushBuildOrder(); + IProgressMonitor sub = subMonitor.newChild(1, SubMonitor.SUPPRESS_SUBTASK); + IStatus saveStatus = workspace.getSaveManager().save(ISaveContext.PROJECT_SAVE, this, sub); + internalClose(subMonitor.newChild(1)); + if (saveStatus != null && !saveStatus.isOK()) { + throw new ResourceException(saveStatus); + } + } + /** * Closes the project. This is called during restore when there is a failure * to read the project description. Since it is called during workspace restore, diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Resource.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Resource.java index 94bfe274d0c..3773950da34 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Resource.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Resource.java @@ -848,6 +848,12 @@ public void delete(int updateFlags, IProgressMonitor monitor) throws CoreExcepti ((Rules) workspace.getRuleFactory()).setRuleFactory((IProject) this, null); // Make sure project deletion is remembered. workspace.getSaveManager().requestSnapshot(); + } else if (getType() == FILE && ((File) this).isProjectDescriptionFile()) { + // a project without description file is closed rather than recreating the file + Project project = (Project) getProject(); + if (project.isOpen() && !getLocalManager().hasSavedDescription(project)) { + project.basicClose(progress.split(1)); + } } } catch (OperationCanceledException e) { workspace.getWorkManager().operationCanceled(); diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java index cac927a0644..b9a213772ce 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java @@ -1114,18 +1114,14 @@ public void standardMoveProject(IProject source, IProjectDescription description } // Move the project content in the local file system. + boolean contentMoved = true; try { moveProjectContent(source, destinationStore, flags, Policy.subMonitorFor(monitor, Policy.totalWork * 3 / 4)); } catch (CoreException e) { message = NLS.bind(Messages.localstore_couldNotMove, source.getFullPath()); IStatus status = new ResourceStatus(IStatus.ERROR, source.getFullPath(), message, e); failed(status); - //refresh the project because it might have been partially moved - try { - source.refreshLocal(IResource.DEPTH_INFINITE, null); - } catch (CoreException e2) { - //ignore secondary failures - } + contentMoved = false; } // If we got this far the project content has been moved on disk (if necessary) @@ -1133,8 +1129,17 @@ public void standardMoveProject(IProject source, IProjectDescription description movedProjectSubtree(source, description); monitor.worked(Policy.totalWork * 1 / 8); + IProject destination = source.getWorkspace().getRoot().getProject(description.getName()); + if (!contentMoved) { + // the content might have been partially moved, so align the tree with the destination + try { + destination.refreshLocal(IResource.DEPTH_INFINITE, null); + } catch (CoreException e) { + //ignore secondary failures + } + } boolean isDeep = (flags & IResource.SHALLOW) == 0; - updateTimestamps(source.getWorkspace().getRoot().getProject(description.getName()), isDeep); + updateTimestamps(destination, isDeep); monitor.worked(Policy.totalWork * 1 / 8); } finally { lock.release(); diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/SaveManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/SaveManager.java index 96d15a2702a..cad8b3fddf0 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/SaveManager.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/SaveManager.java @@ -1311,8 +1311,9 @@ public IStatus save(int kind, boolean keepConsistencyWhenCanceled, Project proje workspace.getFileSystemManager().getHistoryStore().clean(Policy.subMonitorFor(monitor, 1)); monitor.ignoreCancelState(keepConsistencyWhenCanceled); - // write out all metainfo (e.g., workspace/project descriptions) - saveMetaInfo(warnings, Policy.subMonitorFor(monitor, 1)); + // write out the workspace metainfo (e.g., the workspace description) + saveMetaInfo(); + monitor.worked(1); break; case ISaveContext.SNAPSHOT : snapTree(workspace.getElementTree(), Policy.subMonitorFor(monitor, 1)); @@ -1327,8 +1328,9 @@ public IStatus save(int kind, boolean keepConsistencyWhenCanceled, Project proje } collapseTrees(contexts); clearSavedDelta(); - // write out all metainfo (e.g., workspace/project descriptions) - saveMetaInfo(warnings, Policy.subMonitorFor(monitor, 1)); + // write out the workspace metainfo (e.g., the workspace description) + saveMetaInfo(); + monitor.worked(1); break; case ISaveContext.PROJECT_SAVE : writeTree(project, IResource.DEPTH_INFINITE); @@ -1338,10 +1340,6 @@ public IStatus save(int kind, boolean keepConsistencyWhenCanceled, Project proje monitor.worked(1); // reset the snapshot file resetSnapshots(project); - IStatus result = saveMetaInfo(project, null); - if (!result.isOK()) { - warnings.merge(result); - } monitor.worked(1); break; } @@ -1404,52 +1402,20 @@ protected void saveMasterTable(int kind, IPath location) throws CoreException { } /** - * Writes the metainfo (e.g. descriptions) of the given workspace and - * all projects to the local disk. + * Writes the metainfo (e.g. description) of the workspace to the local disk. */ - protected void saveMetaInfo(MultiStatus problems, IProgressMonitor monitor) throws CoreException { + protected void saveMetaInfo() { if (Policy.DEBUG_SAVE_METAINFO) { Policy.debug("Save workspace metainfo: starting..."); //$NON-NLS-1$ } long start = System.currentTimeMillis(); // save preferences (workspace description, path variables, etc) ResourcesPlugin.getPlugin().savePluginPreferences(); - // save projects' meta info - IProject[] roots = workspace.getRoot().getProjects(IContainer.INCLUDE_HIDDEN); - for (IProject root : roots) { - if (root.isAccessible()) { - IStatus result = saveMetaInfo((Project) root, null); - if (!result.isOK()) { - problems.merge(result); - } - } - } if (Policy.DEBUG_SAVE_METAINFO) { Policy.debug("Save workspace metainfo: " + (System.currentTimeMillis() - start) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$ } } - /** - * Ensures that the project meta-info is saved. The project meta-info - * is usually saved as soon as it changes, so this is just a sanity check - * to make sure there is something on disk before we shutdown. - * - * @return Status object containing non-critical warnings, or an OK status. - */ - protected IStatus saveMetaInfo(Project project, IProgressMonitor monitor) throws CoreException { - long start = System.currentTimeMillis(); - //if there is nothing on disk, write the description - if (!workspace.getFileSystemManager().hasSavedDescription(project)) { - workspace.getFileSystemManager().writeSilently(project); - String msg = NLS.bind(Messages.resources_missingProjectMetaRepaired, project.getName()); - return new ResourceStatus(IResourceStatus.MISSING_DESCRIPTION_REPAIRED, project.getFullPath(), msg); - } - if (Policy.DEBUG_SAVE_METAINFO) { - Policy.debug("Save metainfo for " + project.getFullPath() + ": " + (System.currentTimeMillis() - start) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - return Status.OK_STATUS; - } - /** * Writes a snapshot of project refresh information to the specified * location. diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/LocalSyncTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/LocalSyncTest.java index 8aca927fd13..4c68b4f7347 100644 --- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/LocalSyncTest.java +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/LocalSyncTest.java @@ -23,11 +23,9 @@ import static org.eclipse.core.tests.resources.ResourceTestUtil.createInWorkspace; import static org.eclipse.core.tests.resources.ResourceTestUtil.removeFromFileSystem; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.eclipse.core.internal.resources.ICoreConstants; -import org.eclipse.core.internal.resources.TestingSupport; import org.eclipse.core.internal.resources.Workspace; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; @@ -61,9 +59,6 @@ private boolean existsInFileSystemWithNoContent(IResource resource) { @Test public void testProjectDeletion() throws CoreException { - //snapshot will recreate the deleted .project file - TestingSupport.waitForSnapshot(); - // create resources IResource[] resources = buildResources(project, "/File1", "/Folder1/", "/Folder1/File1", "/Folder1/Folder2/"); createInWorkspace(resources); @@ -72,11 +67,12 @@ public void testProjectDeletion() throws CoreException { Workspace.clear(project.getLocation().toFile()); // run synchronize - //The .project file has been deleted, so this will fail - assertThrows(CoreException.class, () -> project.refreshLocal(IResource.DEPTH_INFINITE, null)); + // The .project file has been deleted, so this closes the project + project.refreshLocal(IResource.DEPTH_INFINITE, null); - /* project should still exists */ + /* project should still exist but be closed */ assertTrue(project.exists()); + assertFalse(project.isOpen()); /* resources should not exist anymore */ for (int i = 1; i < resources.length; i++) { diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IProjectDescriptionTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IProjectDescriptionTest.java index e309038f3f8..6be54945c36 100644 --- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IProjectDescriptionTest.java +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IProjectDescriptionTest.java @@ -14,27 +14,44 @@ *******************************************************************************/ package org.eclipse.core.tests.resources; +import static java.util.function.Predicate.not; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace; +import static org.eclipse.core.tests.harness.FileSystemHelper.getRandomLocation; +import static org.eclipse.core.tests.harness.FileSystemHelper.getTempDir; +import static org.eclipse.core.tests.resources.ResourceTestUtil.assertDoesNotExistInFileSystem; import static org.eclipse.core.tests.resources.ResourceTestUtil.createInWorkspace; import static org.eclipse.core.tests.resources.ResourceTestUtil.createTestMonitor; +import static org.eclipse.core.tests.resources.ResourceTestUtil.removeFromFileSystem; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.filesystem.URIUtil; import org.eclipse.core.internal.events.BuildCommand; import org.eclipse.core.internal.resources.Project; import org.eclipse.core.resources.ICommand; import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Status; import org.eclipse.core.tests.internal.builders.CustomTriggerBuilder; +import org.eclipse.core.tests.internal.filesystem.wrapper.WrapperFileStore; +import org.eclipse.core.tests.internal.filesystem.wrapper.WrapperFileSystem; import org.eclipse.core.tests.resources.util.WorkspaceResetExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -51,6 +68,162 @@ public void testDescriptionConstant() { assertEquals(".project", IProjectDescription.DESCRIPTION_FILE_NAME); } + /** + * Deleting the description file through the workspace closes the project and + * does not recreate the file. + */ + @Test + public void testDeleteDescriptionFileClosesProject() throws CoreException { + IProject project = getWorkspace().getRoot().getProject("Project"); + IFile descriptionFile = project.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + createInWorkspace(project); + + descriptionFile.delete(IResource.NONE, createTestMonitor()); + + assertThat(project).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + getWorkspace().save(true, createTestMonitor()); + assertDoesNotExistInFileSystem(descriptionFile); + } + + /** + * A refresh that finds the description file deleted closes the project and + * does not recreate the file. Restoring the file allows to reopen the project. + */ + @Test + public void testRefreshWithDeletedDescriptionFileClosesProject() throws Exception { + IProject project = getWorkspace().getRoot().getProject("Project"); + IFile descriptionFile = project.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + IFile file = project.getFile("file.txt"); + createInWorkspace(file); + Path backup = getTempDir().append("dotProjectBackup").toPath(); + Files.copy(descriptionFile.getLocation().toPath(), backup); + try { + removeFromFileSystem(descriptionFile); + + project.refreshLocal(IResource.DEPTH_INFINITE, createTestMonitor()); + + assertThat(project).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + getWorkspace().save(true, createTestMonitor()); + assertDoesNotExistInFileSystem(descriptionFile); + + Files.copy(backup, descriptionFile.getLocation().toPath()); + project.open(createTestMonitor()); + assertThat(project).matches(IProject::isOpen, "is open"); + assertThat(file).matches(IResource::exists, "exists"); + } finally { + Files.deleteIfExists(backup); + } + } + + /** + * A refresh that finds the whole project directory deleted closes the project + * and does not recreate the directory. + */ + @Test + public void testRefreshWithDeletedProjectDirectoryClosesProject() throws CoreException { + IProject project = getWorkspace().getRoot().getProject("Project"); + createInWorkspace(project.getFile("file.txt")); + + removeFromFileSystem(project); + project.refreshLocal(IResource.DEPTH_INFINITE, createTestMonitor()); + + assertThat(project).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + getWorkspace().save(true, createTestMonitor()); + assertDoesNotExistInFileSystem(project); + } + + /** + * Closing a project does not recreate a deleted description file. + */ + @Test + public void testCloseDoesNotRecreateDescriptionFile() throws CoreException { + IProject project = getWorkspace().getRoot().getProject("Project"); + IFile descriptionFile = project.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + createInWorkspace(project); + + removeFromFileSystem(descriptionFile); + project.close(createTestMonitor()); + + assertThat(project).matches(not(IProject::isOpen), "is closed"); + assertDoesNotExistInFileSystem(descriptionFile); + } + + /** + * Moves by copying and deleting, and refuses to delete a file named + * {@link #UNDELETABLE_FILE} after deleting everything else, like a locked + * file on Windows. + */ + public static class UndeletableFileStore extends WrapperFileStore { + static final String UNDELETABLE_FILE = "undeletable.txt"; + + public UndeletableFileStore(IFileStore store) { + super(store); + } + + @Override + public void move(IFileStore destination, int options, IProgressMonitor monitor) throws CoreException { + copy(destination, options, monitor); + delete(EFS.NONE, monitor); + } + + @Override + public void delete(int options, IProgressMonitor monitor) throws CoreException { + CoreException failure = null; + for (IFileStore child : childStores(EFS.NONE, null)) { + try { + child.delete(options, monitor); + } catch (CoreException e) { + failure = e; + } + } + if (failure != null) { + throw failure; + } + if (UNDELETABLE_FILE.equals(getName())) { + throw new CoreException(Status.error("cannot delete " + this)); + } + super.delete(options, monitor); + } + } + + /** + * A project move that copied the content and then failed to delete part of + * the source, including its description file, still ends up at the + * destination with its markers. + */ + @Test + public void testMoveWithUndeletableSourceContent() throws Exception { + IPath sourceLocation = getRandomLocation(); + IProject source = getWorkspace().getRoot().getProject("Source"); + IProjectDescription sourceDescription = getWorkspace().newProjectDescription(source.getName()); + sourceDescription.setLocationURI(WrapperFileSystem.getWrappedURI(URIUtil.toURI(sourceLocation))); + source.create(sourceDescription, createTestMonitor()); + source.open(createTestMonitor()); + IFile file = source.getFile("file.txt"); + IFile undeletableFile = source.getFile(UndeletableFileStore.UNDELETABLE_FILE); + createInWorkspace(new IResource[] { file, undeletableFile }); + IMarker marker = file.createMarker(IMarker.BOOKMARK); + IProject destination = getWorkspace().getRoot().getProject("Destination"); + IProjectDescription destinationDescription = getWorkspace().newProjectDescription(destination.getName()); + WrapperFileSystem.setCustomFileStore(UndeletableFileStore.class); + try { + assertThrows(CoreException.class, + () -> source.move(destinationDescription, IResource.FORCE, createTestMonitor())); + + assertFalse(sourceLocation.append(IProjectDescription.DESCRIPTION_FILE_NAME).toFile().exists()); + assertThat(source).matches(not(IProject::exists), "does not exist"); + assertThat(destination).matches(IProject::isOpen, "is open"); + IFile movedFile = destination.getFile(file.getProjectRelativePath()); + assertThat(movedFile).matches(IResource::exists, "exists"); + assertNotNull(movedFile.findMarker(marker.getId())); + assertThat(destination.getFile(undeletableFile.getProjectRelativePath())).matches(IResource::exists, + "exists"); + } finally { + WrapperFileSystem.setCustomFileStore(null); + removeFromFileSystem(sourceLocation.toFile()); + } + } + /** * Tests that setting the build spec preserves any instantiated builder. */ diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IProjectTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IProjectTest.java index 113fa4ee5c8..134e0a4dccb 100644 --- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IProjectTest.java +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IProjectTest.java @@ -29,6 +29,7 @@ import static org.eclipse.core.tests.resources.ResourceTestUtil.createRandomString; import static org.eclipse.core.tests.resources.ResourceTestUtil.createUniqueString; import static org.eclipse.core.tests.resources.ResourceTestUtil.getLineSeparatorFromFile; +import static org.eclipse.core.tests.resources.ResourceTestUtil.removeFromFileSystem; import static org.eclipse.core.tests.resources.ResourceTestUtil.removeFromWorkspace; import static org.eclipse.core.tests.resources.ResourceTestUtil.waitForRefresh; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -582,18 +583,13 @@ public void testProjectCreationLineSeparator() throws BackingStoreException, Cor Preferences projectNode = rootNode.node(ProjectScope.SCOPE).node(project.getName()).node(Platform.PI_RUNTIME); projectNode.put(Platform.PREF_LINE_SEPARATOR, newProjectValue); projectNode.flush(); - // remove .project file but leave the project - monitor.prepare(); - file.delete(true, monitor); - monitor.assertUsedUp(); - assertFalse(file.exists()); - // workspace save should recreate .project file with project-specific line delimiter - monitor.prepare(); - getWorkspace().save(true, monitor); - monitor.assertUsedUp(); - // refresh project to update the resource tree + // remove .project file from disk but leave the project + removeFromFileSystem(file); + // writing the description should recreate .project file with project-specific line delimiter + description = project.getDescription(); + description.setComment("another comment"); monitor.prepare(); - project.refreshLocal(IResource.DEPTH_INFINITE, monitor); + project.setDescription(description, IResource.FORCE, monitor); monitor.assertUsedUp(); assertTrue(file.exists()); // new .project should have project-specific line separator diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/ISynchronizerTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/ISynchronizerTest.java index 19b9471d151..1b46a9b2ab2 100644 --- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/ISynchronizerTest.java +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/ISynchronizerTest.java @@ -155,13 +155,15 @@ public void testDeleteResources() throws CoreException { }; getWorkspace().getRoot().accept(visitor); - // delete all resources under the projects. + // delete all resources under the projects, deleting .project would close them final IProject[] projects = getWorkspace().getRoot().getProjects(); IWorkspaceRunnable body = monitor -> { for (IProject project : projects) { IResource[] children = project.members(); for (IResource element : children) { - element.delete(false, createTestMonitor()); + if (!IProjectDescription.DESCRIPTION_FILE_NAME.equals(element.getName())) { + element.delete(false, createTestMonitor()); + } } } }; diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IWorkspaceTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IWorkspaceTest.java index 19ad9e3054c..27a00b1b0e7 100644 --- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IWorkspaceTest.java +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/IWorkspaceTest.java @@ -715,19 +715,21 @@ public void testMultiSetDescription() throws CoreException { } /** - * Test API method IWorkspace.setDescription. + * Test API method IWorkspace.save. */ @Test public void testSave() throws CoreException { - // ensure save returns a warning if a project's .project file is deleted. + // deleting a project's .project file closes the project, save must not recreate the file IProject project = getWorkspace().getRoot().getProject("Broken"); createInWorkspace(project); // wait for snapshot before modifying file TestingSupport.waitForSnapshot(); IFile descriptionFile = project.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); descriptionFile.delete(IResource.NONE, null); + assertFalse(project.isOpen()); IStatus result = getWorkspace().save(true, createTestMonitor()); - assertEquals(IStatus.WARNING, result.getSeverity()); + assertTrue(result.isOK()); + assertDoesNotExistInFileSystem(descriptionFile); } /** diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/session/TestBug12575.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/session/TestBug12575.java index 9de75a504b2..77f689e6646 100644 --- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/session/TestBug12575.java +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/session/TestBug12575.java @@ -13,8 +13,11 @@ *******************************************************************************/ package org.eclipse.core.tests.resources.session; +import static java.util.function.Predicate.not; +import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace; import static org.eclipse.core.tests.resources.ResourceTestPluginConstants.PI_RESOURCES_TESTS; +import static org.eclipse.core.tests.resources.ResourceTestUtil.assertDoesNotExistInFileSystem; import static org.eclipse.core.tests.resources.ResourceTestUtil.createInWorkspace; import static org.eclipse.core.tests.resources.ResourceTestUtil.createTestMonitor; @@ -39,8 +42,8 @@ public class TestBug12575 { .withCustomization(SessionTestExtension.createCustomWorkspace()).create(); /** - * Setup. Create a simple project, delete the .project file, shutdown - * cleanly. + * Setup. Create a simple project, delete the .project file, which closes the + * project, shutdown cleanly. */ @Test @Order(1) @@ -50,20 +53,23 @@ public void test1() throws CoreException { project.open(createTestMonitor()); IFile dotProject = project.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); dotProject.delete(IResource.NONE, createTestMonitor()); + assertThat(project).matches(not(IProject::isOpen), "is closed"); getWorkspace().save(true, createTestMonitor()); + assertDoesNotExistInFileSystem(dotProject); } /** - * Infection. Modify the .project, cause a snapshot, crash + * Infection. The project is still closed and without .project file. Delete + * it, cause a snapshot, crash */ @Test @Order(2) public void test2() throws CoreException { IProject project = getWorkspace().getRoot().getProject(projectName); IProject other = getWorkspace().getRoot().getProject("Other"); - IProjectDescription desc = project.getDescription(); - desc.setReferencedProjects(new IProject[] { other }); - project.setDescription(desc, IResource.FORCE, createTestMonitor()); + assertThat(project).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + assertDoesNotExistInFileSystem(project.getFile(IProjectDescription.DESCRIPTION_FILE_NAME)); + project.delete(true, createTestMonitor()); //creating a project will cause a snapshot createInWorkspace(other); diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/usecase/Snapshot2Test.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/usecase/Snapshot2Test.java index 31a398aa267..03a3216bfca 100644 --- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/usecase/Snapshot2Test.java +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/usecase/Snapshot2Test.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.List; import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; @@ -70,8 +71,10 @@ public void testChangeProject2() throws CoreException { assertTrue(project.exists()); assertTrue(project.isOpen()); - // remove all resources - IResource[] children = project.members(); + // remove all resources but .project, deleting it would close the project + IResource[] children = Arrays.stream(project.members()) + .filter(child -> !IProjectDescription.DESCRIPTION_FILE_NAME.equals(child.getName())) + .toArray(IResource[]::new); getWorkspace().delete(children, true, null); // create some children