From 3e0806bc775cd6a227ac0eade5dd4b60aba47f96 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Wed, 2 Sep 2026 13:31:20 +0200 Subject: [PATCH] Close a project when its .project file is deleted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the project description file disappeared, the workspace kept the project open with the in-memory description, logged a refresh error and silently wrote the description back to disk at the next snapshot or on close. Switching to a git branch that no longer contains a project thus left a dirty working tree with a recreated .project file. A refresh that finds the description file deleted, and deleting the file through the workspace, now close the project instead. Closing a project and saving the workspace no longer recreate a missing description file. Opening the project again works once the file is back, as for a project with a missing description on startup. A refresh of the project closes it before visiting any member: deleting a member with linked resources or filters writes the description back to disk, and a refresh canceled after the description file left the workspace tree would otherwise leave the project open with nothing left to rediscover the deletion. Removing linked resources or filters of a project whose description file is gone no longer writes the file back either, which also covers a nested project refreshed through its parent. Deleting the description file closes the project even when the operation is canceled afterwards, also for a nested project deleted through its parent, since the update of the aliases of a deleted resource can no longer be skipped by a cancellation. A project move whose content could only be moved partially now refreshes the destination after the tree has been moved, instead of the source before. The tree always ends up under the destination name, so the source refresh described the destination with the leftovers of the source: files already deleted there were dropped from the tree with their markers even though they exist at the destination, and with this change the source refresh would also have closed the project because its description file is gone. The destination refresh keeps the moved resources and their markers and trims what was never copied. Fixes https://github.com/eclipse-platform/eclipse.platform/issues/1074 Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../localstore/FileSystemResourceManager.java | 43 +- .../localstore/RefreshLocalVisitor.java | 43 +- .../eclipse/core/internal/resources/File.java | 16 +- .../core/internal/resources/Project.java | 34 +- .../core/internal/resources/Resource.java | 41 +- .../core/internal/resources/ResourceTree.java | 19 +- .../core/internal/resources/SaveManager.java | 60 +-- .../eclipse/core/internal/utils/Messages.java | 1 + .../core/internal/utils/messages.properties | 1 + .../internal/localstore/LocalSyncTest.java | 12 +- .../resources/IProjectDescriptionTest.java | 377 ++++++++++++++++++ .../core/tests/resources/IProjectTest.java | 18 +- .../tests/resources/ISynchronizerTest.java | 6 +- .../core/tests/resources/IWorkspaceTest.java | 8 +- .../tests/resources/session/TestBug12575.java | 18 +- .../resources/usecase/Snapshot2Test.java | 7 +- 16 files changed, 561 insertions(+), 143 deletions(-) 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 487593b91b3..5b284ce55cb 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 @@ -1103,7 +1103,12 @@ protected boolean refreshResource(IResource target, int depth, boolean updateAli UnifiedTree tree = fileTree == null ? new UnifiedTree(target) : new UnifiedTree(target, fileTree); SubMonitor refreshMonitor = subMonitor.newChild(98); RefreshLocalVisitor visitor = updateAliases ? new RefreshLocalAliasVisitor(refreshMonitor) : new RefreshLocalVisitor(refreshMonitor); - tree.accept(visitor, depth); + try { + tree.accept(visitor, depth); + } finally { + // the description file is gone from the tree even when the refresh was canceled + visitor.closeProjectsWithoutDescription(); + } IStatus result = visitor.getErrorStatus(); if (!result.isOK()) { throw new ResourceException(result); @@ -1476,42 +1481,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 7279927515e..a779a4212ba 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); @@ -107,6 +111,13 @@ protected void deleteResource(UnifiedTreeNode node, Resource target) throws Core } if (target.exists(flags, false)) { target.deleteResource(true, errors); + // a filtered description file is still read from disk + if (target.getType() == IResource.FILE && ((File) target).isProjectDescriptionFile()) { + Project project = (Project) target.getProject(); + if (project.isOpen() && !target.getLocalManager().hasSavedDescription(project)) { + projectsWithoutDescription.add(project); + } + } } node.setExistsWorkspace(false); } @@ -146,6 +157,29 @@ protected void folderToFile(UnifiedTreeNode node, Resource target) throws CoreEx target.getLocalManager().updateLocalSync(info, node.getLastModified()); } + /** + * Closes the open projects whose description file was found deleted, rather + * than recreating the file. Failures are added to the error status. + */ + public void closeProjectsWithoutDescription() { + for (Project project : projectsWithoutDescription) { + closeProjectWithoutDescription(project); + } + } + + private void closeProjectWithoutDescription(Project project) { + if (!project.isOpen()) { + return; + } + String message = NLS.bind(Messages.resources_missingProjectMetaClosed, project.getName()); + Policy.log(new ResourceStatus(IStatus.WARNING, IResourceStatus.FAILED_READ_METADATA, project.getFullPath(), message, null)); + try { + project.basicClose(null); + } catch (CoreException e) { + errors.merge(e.getStatus()); + } + } + /** * 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 @@ -287,6 +321,12 @@ public boolean visit(UnifiedTreeNode node) throws CoreException { Resource target = (Resource) node.getResource(); int targetType = target.getType(); if (targetType == IResource.PROJECT) { + // close before visiting the members, whose deletion could write the description back + Project project = (Project) target; + if (project.isOpen() && !target.getLocalManager().hasSavedDescription(project)) { + closeProjectWithoutDescription(project); + return false; + } return true; } if (node.existsInWorkspace() && node.existsInFileSystem()) { @@ -318,7 +358,8 @@ public boolean visit(UnifiedTreeNode node) throws CoreException { } int state = synchronizeExistence(node, target); if (state == RL_IN_SYNC || state == RL_NOT_IN_SYNC) { - if (targetType == IResource.FILE) { + // the metadata of a project about to be closed is not read + if (targetType == IResource.FILE && !projectsWithoutDescription.contains(target.getProject())) { try { ((File) target).updateMetadataFiles(); } catch (CoreException e) { 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..0a6f68a93d2 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,27 @@ private boolean shouldBuild() { workspace.run(buildRunnable, null, IWorkspace.AVOID_UPDATE, monitor); } + /** + * Closes this open project. Must be called from within a workspace operation; + * no further scheduling rule is acquired, since a nested project may be closed + * under the rule of the project that contains it. + */ + 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, false, this, null, 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..98d9e2da8df 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 @@ -800,6 +800,7 @@ public void delete(int updateFlags, IProgressMonitor monitor) throws CoreExcepti progress.subTask(message); final ISchedulingRule rule = workspace.getRuleFactory().deleteRule(this); SubMonitor split = progress.split(1); + boolean deletesDescriptionFile = false; try { workspace.prepareOperation(rule, split); // If there is no resource then there is nothing to delete so just return. @@ -807,6 +808,7 @@ public void delete(int updateFlags, IProgressMonitor monitor) throws CoreExcepti return; } workspace.beginOperation(true); + deletesDescriptionFile = getType() == FILE && ((File) this).isProjectDescriptionFile(); broadcastPreDeleteEvent(); // When a project is being deleted, flush the build order in case there is a problem. @@ -841,7 +843,8 @@ public void delete(int updateFlags, IProgressMonitor monitor) throws CoreExcepti // Update any aliases of this resource. // Note that deletion of a linked resource cannot affect other resources. if (!wasLinked) { - workspace.getAliasManager().updateAliases(this, originalStore, IResource.DEPTH_INFINITE, progress.split(48)); + // not cancelable, the aliases have to follow the deletion done on disk + workspace.getAliasManager().updateAliases(this, originalStore, IResource.DEPTH_INFINITE, progress.newChild(48)); } if (getType() == PROJECT) { // Make sure the rule factory is cleared on project deletion. @@ -853,8 +856,36 @@ public void delete(int updateFlags, IProgressMonitor monitor) throws CoreExcepti workspace.getWorkManager().operationCanceled(); throw e; } finally { - progress.done(); - workspace.endOperation(rule, true); + try { + // also when canceled or failed after the file was deleted + if (deletesDescriptionFile) { + closeProjectWithoutDescription(); + } + } finally { + progress.done(); + workspace.endOperation(rule, true); + } + } + } + + /** + * Closes the open project of this description file if its description file + * is gone, rather than recreating the file. + */ + private void closeProjectWithoutDescription() throws CoreException { + Project project = (Project) getProject(); + if (project.isOpen() && !getLocalManager().hasSavedDescription(project)) { + project.basicClose(null); + } + } + + /** + * Writes the description of the given project unless its description file is + * gone, since such a project gets closed instead of recreating the file. + */ + private void writeDescriptionIfSaved(Project project) throws CoreException { + if (getLocalManager().hasSavedDescription(project)) { + project.writeDescription(IResource.FORCE); } } @@ -908,7 +939,7 @@ public void deleteResource(boolean convertToPhantom, MultiStatus status) throws if (wasChanged) { project.internalSetDescription(description, true); try { - project.writeDescription(IResource.FORCE); + writeDescriptionIfSaved(project); } catch (CoreException e) { // A problem happened updating the description, update the description in memory. project.updateDescription(); @@ -936,7 +967,7 @@ public void deleteResource(boolean convertToPhantom, MultiStatus status) throws description.setFilters(resource.getProjectRelativePath(), null); } project.internalSetDescription(description, true); - project.writeDescription(IResource.FORCE); + writeDescriptionIfSaved(project); } } 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 e718b62f705..cb71bffd30a 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 @@ -1261,6 +1261,15 @@ public IStatus save(int kind, Project project, IProgressMonitor monitor) throws } public IStatus save(int kind, boolean keepConsistencyWhenCanceled, Project project, IProgressMonitor parentMonitor) throws CoreException { + ISchedulingRule rule = project != null ? (IResource) project : workspace.getRoot(); + return save(kind, keepConsistencyWhenCanceled, project, rule, parentMonitor); + } + + /** + * Saves under the given scheduling rule, which is {@code null} when the + * caller is already inside an operation that covers the project. + */ + IStatus save(int kind, boolean keepConsistencyWhenCanceled, Project project, ISchedulingRule rule, IProgressMonitor parentMonitor) throws CoreException { InternalMonitorWrapper monitor = new InternalMonitorWrapper(parentMonitor); monitor.ignoreCancelState(keepConsistencyWhenCanceled); try { @@ -1269,7 +1278,6 @@ public IStatus save(int kind, boolean keepConsistencyWhenCanceled, Project proje monitor.beginTask(message, 7); message = Messages.resources_saveWarnings; MultiStatus warnings = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IStatus.WARNING, message, null); - ISchedulingRule rule = project != null ? (IResource) project : workspace.getRoot(); try { workspace.prepareOperation(rule, monitor); workspace.beginOperation(false); @@ -1308,8 +1316,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)); @@ -1324,8 +1333,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); @@ -1335,10 +1345,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; } @@ -1401,52 +1407,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/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java index 000805ee5de..6ab62761981 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java @@ -214,6 +214,7 @@ public class Messages extends NLS { public static String resources_invalidRoot; public static String resources_markerNotFound; public static String resources_missingProjectMeta; + public static String resources_missingProjectMetaClosed; public static String resources_missingProjectMetaRepaired; public static String resources_moveDestNotSub; public static String resources_moveMeta; diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties index b67c2c0eea1..82b4cd3d16b 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties @@ -215,6 +215,7 @@ resources_invalidResourceName = ''{0}'' is an invalid resource name. resources_invalidRoot = Root (/) is an invalid resource path. resources_markerNotFound = Marker id {0} not found. resources_missingProjectMeta = The project description file (.project) for ''{0}'' is missing. This file contains important information about the project. The project will not function properly until this file is restored. +resources_missingProjectMetaClosed = The project description file (.project) for ''{0}'' is missing. The project has been closed and can be reopened once the file is restored. resources_missingProjectMetaRepaired = The project description file (.project) for ''{0}'' was missing. This file contains important information about the project. A new project description file has been created, but some information about the project may have been lost. resources_moveDestNotSub = Cannot move ''{0}''. Destination should not be under source''s hierarchy. resources_moveMeta = Error moving metadata area from {0} to {1}. 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..0ce2253ca6c 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,49 @@ *******************************************************************************/ 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.FileInfoMatcherDescription; import org.eclipse.core.resources.ICommand; import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +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.IResourceFilterDescription; 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.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +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 +73,361 @@ 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); + IMarker marker = file.createMarker(IMarker.BOOKMARK); + 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"); + assertNotNull(file.findMarker(marker.getId())); + } finally { + Files.deleteIfExists(backup); + } + } + + /** + * Members with links or filters deleted in the same refresh as the + * description file do not write the description file back. + */ + @Test + public void testRefreshWithDeletedDescriptionFileDoesNotRecreateItForLinksAndFilters() throws Exception { + IProject project = getWorkspace().getRoot().getProject("Project"); + IFile descriptionFile = project.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + IFolder linkParent = project.getFolder("linkParent"); + IFolder filtered = project.getFolder("filtered"); + createInWorkspace(new IResource[] { linkParent, filtered }); + IPath linkTarget = getRandomLocation(); + Files.writeString(Files.createDirectories(linkTarget.toPath()).resolve("linked.txt"), "linked"); + try { + linkParent.getFile("linked.txt").createLink(linkTarget.append("linked.txt"), IResource.NONE, createTestMonitor()); + filtered.createFilter(IResourceFilterDescription.EXCLUDE_ALL | IResourceFilterDescription.FILES, + new FileInfoMatcherDescription("org.eclipse.core.resources.regexFilterMatcher", "ignored"), 0, + createTestMonitor()); + + removeFromFileSystem(descriptionFile); + removeFromFileSystem(linkParent); + removeFromFileSystem(filtered); + project.refreshLocal(IResource.DEPTH_INFINITE, createTestMonitor()); + + assertThat(project).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + assertDoesNotExistInFileSystem(descriptionFile); + } finally { + removeFromFileSystem(linkTarget.toFile()); + } + } + + /** + * A refresh canceled while the description file is being found deleted still + * closes the project instead of leaving it open without description. + */ + @Test + public void testCanceledRefreshWithDeletedDescriptionFileClosesProject() throws CoreException { + IProject project = getWorkspace().getRoot().getProject("Project"); + IFile descriptionFile = project.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + createInWorkspace(project.getFile("file.txt")); + IProgressMonitor cancelOnceDescriptionIsGone = new NullProgressMonitor() { + @Override + public boolean isCanceled() { + return !descriptionFile.exists(); + } + }; + + removeFromFileSystem(descriptionFile); + try { + project.refreshLocal(IResource.DEPTH_INFINITE, cancelOnceDescriptionIsGone); + } catch (OperationCanceledException e) { + // the refresh may or may not reach a cancellation check after the deletion + } + + assertThat(project).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + assertDoesNotExistInFileSystem(descriptionFile); + } + + /** + * 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); + } + + /** + * Deleting the description file of a nested project through the parent + * project closes the nested project, although the operation only holds a + * rule on the parent. + */ + @Test + public void testDeleteNestedDescriptionFileThroughParentClosesNestedProject() throws CoreException { + IProject parent = getWorkspace().getRoot().getProject("parent"); + IProject child = getWorkspace().getRoot().getProject("child"); + createInWorkspace(parent); + IProjectDescription childDescription = getWorkspace().newProjectDescription(child.getName()); + childDescription.setLocation(parent.getLocation().append(child.getName())); + child.create(childDescription, createTestMonitor()); + child.open(createTestMonitor()); + IFile childDescriptionFileInParent = parent.getFolder(child.getName()) + .getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + assertThat(childDescriptionFileInParent).matches(IResource::exists, "exists"); + + childDescriptionFileInParent.delete(IResource.NONE, createTestMonitor()); + + assertThat(child).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + assertThat(parent).matches(IProject::isOpen, "is open"); + } + + /** + * Deleting the description file of a nested project through the parent + * project closes the nested project also when the operation is canceled + * after the file was deleted. + */ + @Test + public void testCanceledDeleteOfNestedDescriptionFileThroughParentClosesNestedProject() throws CoreException { + IProject parent = getWorkspace().getRoot().getProject("parent"); + IProject child = getWorkspace().getRoot().getProject("child"); + createInWorkspace(parent); + IProjectDescription childDescription = getWorkspace().newProjectDescription(child.getName()); + childDescription.setLocation(parent.getLocation().append(child.getName())); + child.create(childDescription, createTestMonitor()); + child.open(createTestMonitor()); + IFile childDescriptionFileInParent = parent.getFolder(child.getName()) + .getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + IProgressMonitor cancelOnceDescriptionIsGone = new NullProgressMonitor() { + @Override + public boolean isCanceled() { + return !childDescriptionFileInParent.exists(); + } + }; + + try { + childDescriptionFileInParent.delete(IResource.NONE, cancelOnceDescriptionIsGone); + } catch (OperationCanceledException e) { + // the delete may or may not reach a cancellation check after the deletion + } + + assertThat(child).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + assertDoesNotExistInFileSystem(child.getFile(IProjectDescription.DESCRIPTION_FILE_NAME)); + assertThat(parent).matches(IProject::isOpen, "is open"); + } + + /** + * Refreshing the parent project after the description file of a nested + * project disappeared on disk closes the nested project. + */ + @Test + public void testRefreshParentWithDeletedNestedDescriptionFileClosesNestedProject() throws CoreException { + IProject parent = getWorkspace().getRoot().getProject("parent"); + IProject child = getWorkspace().getRoot().getProject("child"); + createInWorkspace(parent); + IProjectDescription childDescription = getWorkspace().newProjectDescription(child.getName()); + childDescription.setLocation(parent.getLocation().append(child.getName())); + child.create(childDescription, createTestMonitor()); + child.open(createTestMonitor()); + IFile childDescriptionFile = child.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + + removeFromFileSystem(childDescriptionFile); + parent.refreshLocal(IResource.DEPTH_INFINITE, createTestMonitor()); + + assertThat(child).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + assertThat(parent).matches(IProject::isOpen, "is open"); + } + + /** + * Refreshing the parent project does not write the description file of a + * nested project back when a member of the nested project with links is + * deleted in the same refresh. + */ + @Test + public void testRefreshParentWithDeletedNestedDescriptionFileDoesNotRecreateItForLinks() throws Exception { + IProject parent = getWorkspace().getRoot().getProject("parent"); + IProject child = getWorkspace().getRoot().getProject("child"); + createInWorkspace(parent); + IProjectDescription childDescription = getWorkspace().newProjectDescription(child.getName()); + childDescription.setLocation(parent.getLocation().append(child.getName())); + child.create(childDescription, createTestMonitor()); + child.open(createTestMonitor()); + IFile childDescriptionFile = child.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + IFolder linkParent = child.getFolder("linkParent"); + createInWorkspace(linkParent); + IPath linkTarget = getRandomLocation(); + Files.writeString(Files.createDirectories(linkTarget.toPath()).resolve("linked.txt"), "linked"); + try { + linkParent.getFile("linked.txt").createLink(linkTarget.append("linked.txt"), IResource.NONE, createTestMonitor()); + + removeFromFileSystem(childDescriptionFile); + removeFromFileSystem(linkParent); + parent.refreshLocal(IResource.DEPTH_INFINITE, createTestMonitor()); + + assertThat(child).matches(IProject::exists, "exists").matches(not(IProject::isOpen), "is closed"); + assertDoesNotExistInFileSystem(childDescriptionFile); + } finally { + removeFromFileSystem(linkTarget.toFile()); + } + } + + /** + * Deleting the description file through the workspace closes the project also + * when the operation is canceled after the file was deleted. + */ + @Test + public void testCanceledDeleteOfDescriptionFileClosesProject() throws CoreException { + IProject project = getWorkspace().getRoot().getProject("Project"); + IFile descriptionFile = project.getFile(IProjectDescription.DESCRIPTION_FILE_NAME); + createInWorkspace(project); + IProgressMonitor cancelOnceDescriptionIsGone = new NullProgressMonitor() { + @Override + public boolean isCanceled() { + return !descriptionFile.exists(); + } + }; + + try { + descriptionFile.delete(IResource.NONE, cancelOnceDescriptionIsGone); + } catch (OperationCanceledException e) { + // the delete may or may not reach a cancellation check after the deletion + } + + assertThat(project).matches(IProject::exists, "exists").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 63bf3032650..f9fc36fc261 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 = _ -> { 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 d4234c4d19a..11d44c7c902 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