Dealing with project deleted on file system by user.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.tooling.jdt.ls.commons.test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -21,11 +22,11 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
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.resources.ResourcesPlugin;
|
||||
import org.eclipse.core.resources.WorkspaceJob;
|
||||
import org.eclipse.core.runtime.CoreException;
|
||||
@@ -38,26 +39,29 @@ import org.eclipse.core.runtime.Platform;
|
||||
import org.eclipse.core.runtime.Status;
|
||||
import org.eclipse.jdt.core.JavaCore;
|
||||
import org.junit.After;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.tooling.jdt.ls.commons.Logger;
|
||||
import org.springframework.tooling.jdt.ls.commons.classpath.Classpath;
|
||||
import org.springframework.tooling.jdt.ls.commons.classpath.Classpath.CPE;
|
||||
import org.springframework.tooling.jdt.ls.commons.classpath.ClientCommandExecutor;
|
||||
import org.springframework.tooling.jdt.ls.commons.classpath.ReusableClasspathListenerHandler;
|
||||
import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition;
|
||||
import org.springsource.ide.eclipse.commons.frameworks.test.util.Asserter;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
public class ClasspathListenerHandlerTest {
|
||||
|
||||
|
||||
private MockClasspathCache classpaths = new MockClasspathCache();
|
||||
private ReusableClasspathListenerHandler service = new ReusableClasspathListenerHandler(classpaths);
|
||||
|
||||
|
||||
@Test public void classpathIsSentForExistingProject() throws Exception {
|
||||
String projectName = "classpath-test-simple-java-project";
|
||||
IProject project = createTestProject(projectName);
|
||||
File loc = project.getLocation().toFile();
|
||||
|
||||
|
||||
service.addClasspathListener(classpaths.commandId);
|
||||
ACondition.waitFor("Project with classpath to appear", Duration.ofSeconds(50), () -> {
|
||||
Classpath cp = classpaths.getFor(loc).classpath;
|
||||
@@ -69,7 +73,7 @@ public class ClasspathListenerHandlerTest {
|
||||
|
||||
@Test public void classpathIsSentForNewProject_and_removedForDeletedProject() throws Exception {
|
||||
service.addClasspathListener(classpaths.commandId);
|
||||
|
||||
|
||||
String projectName = "classpath-test-simple-java-project";
|
||||
IProject project = createTestProject(projectName);
|
||||
File loc = project.getLocation().toFile();
|
||||
@@ -84,15 +88,37 @@ public class ClasspathListenerHandlerTest {
|
||||
Info cp = classpaths.getFor(loc);
|
||||
assertNull(cp);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Test public void classpathIsRemovedWhenProjectDeletedFromFileSystem() throws Exception {
|
||||
String projectName = "classpath-test-simple-java-project";
|
||||
IProject project = createTestProject(projectName);
|
||||
File loc = project.getLocation().toFile();
|
||||
|
||||
service.addClasspathListener(classpaths.commandId);
|
||||
ACondition.waitFor("Project with classpath to appear", Duration.ofSeconds(50), () -> {
|
||||
Classpath cp = classpaths.getFor(loc).classpath;
|
||||
assertTrue(cp.getEntries().stream().filter(cpe -> Classpath.isSource(cpe)).count()==1); //has 1 source entry
|
||||
assertClasspath(cp, cp.getEntries().stream().filter(cpe -> Classpath.isBinary(cpe) && cpe.isSystem()).count()>=1); //has some system libraries
|
||||
});
|
||||
|
||||
FileUtils.deleteQuietly(loc);
|
||||
safe(() -> project.refreshLocal(IResource.DEPTH_INFINITE, null));
|
||||
|
||||
ACondition.waitFor("Project to disapear", Duration.ofSeconds(50), () -> {
|
||||
Info cp = classpaths.getFor(loc);
|
||||
assertNull(cp);
|
||||
});
|
||||
}
|
||||
|
||||
///////////// harness stuff below ///////////////////////////////////////////////
|
||||
|
||||
|
||||
@Rule public TemporaryFolder tmp = new TemporaryFolder();
|
||||
|
||||
static class Info {
|
||||
public final String name;
|
||||
public final Classpath classpath;
|
||||
|
||||
|
||||
private Info(String name, Classpath cp) {
|
||||
super();
|
||||
this.name = name;
|
||||
@@ -103,9 +129,9 @@ public class ClasspathListenerHandlerTest {
|
||||
public class MockClasspathCache implements ClientCommandExecutor {
|
||||
|
||||
String commandId = RandomStringUtils.randomAlphabetic(8);
|
||||
|
||||
|
||||
Map<File, Info> classpaths = new HashMap<>();
|
||||
|
||||
|
||||
@Override
|
||||
public synchronized Object executeClientCommand(String id, Object... params) throws Exception {
|
||||
if (id.equals(commandId)) {
|
||||
@@ -131,17 +157,17 @@ public class ClasspathListenerHandlerTest {
|
||||
|
||||
public void dispose() throws Exception {
|
||||
service.removeClasspathListener(commandId);
|
||||
deleteAllProjects();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
classpaths.dispose();
|
||||
deleteAllProjects();
|
||||
assertTrue(service.hasNoActiveSubscriptions());
|
||||
}
|
||||
|
||||
|
||||
private static void assertClasspath(Classpath cp, boolean b) {
|
||||
if (!b) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
@@ -154,40 +180,56 @@ public class ClasspathListenerHandlerTest {
|
||||
}
|
||||
|
||||
private IProject createTestProject(String name) throws Exception {
|
||||
File testProjectLocation = new File(FileLocator.toFileURL(Platform.getBundle("org.springframework.tooling.jdt.ls.commons.test").getEntry("test-projects/"+name)).toURI());
|
||||
File testProjectSourceLocation = new File(FileLocator.toFileURL(Platform.getBundle("org.springframework.tooling.jdt.ls.commons.test").getEntry("test-projects/"+name)).toURI());
|
||||
|
||||
File testProjectLocation = tmp.newFolder(name);
|
||||
FileUtils.copyDirectory(testProjectSourceLocation, testProjectLocation);
|
||||
|
||||
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(name);
|
||||
IProjectDescription desc = ResourcesPlugin.getWorkspace().newProjectDescription(null);
|
||||
desc.setName(name);
|
||||
desc.setLocation(Path.fromOSString(testProjectLocation.toString()));
|
||||
project.create(desc, null);
|
||||
project.open(null);
|
||||
|
||||
|
||||
assertTrue(project.hasNature(JavaCore.NATURE_ID));
|
||||
return project;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static void deleteAllProjects() throws Exception {
|
||||
CompletableFuture<Void> done = new CompletableFuture<Void>();
|
||||
WorkspaceJob job = new WorkspaceJob("Delete projects") {
|
||||
@Override public IStatus runInWorkspace(IProgressMonitor arg0) throws CoreException {
|
||||
try {
|
||||
ResourcesPlugin.getWorkspace().getRuleFactory().buildRule();
|
||||
IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
|
||||
for (IProject project : allProjects) {
|
||||
project.refreshLocal(IResource.DEPTH_INFINITE, null);
|
||||
project.close(null);
|
||||
project.delete(false, true, new NullProgressMonitor());
|
||||
}
|
||||
ACondition.waitFor("Deleting all projects", Duration.ofMinutes(1), () -> {
|
||||
ResourcesPlugin.getWorkspace().getRuleFactory().buildRule();
|
||||
IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
|
||||
for (IProject project : allProjects) {
|
||||
project.refreshLocal(IResource.DEPTH_INFINITE, null);
|
||||
safe(() -> project.close(null));
|
||||
project.delete(false, true, new NullProgressMonitor());
|
||||
assertFalse(project.exists());
|
||||
}
|
||||
});
|
||||
done.complete(null);
|
||||
} catch (Throwable e) {
|
||||
done.completeExceptionally(e);
|
||||
}
|
||||
return Status.OK_STATUS;
|
||||
}
|
||||
|
||||
};
|
||||
job.setRule(ResourcesPlugin.getWorkspace().getRuleFactory().buildRule());
|
||||
job.schedule();
|
||||
done.get();
|
||||
}
|
||||
private static void safe(Asserter doit) {
|
||||
try {
|
||||
doit.execute();
|
||||
} catch (Throwable e) {
|
||||
System.err.println("Ignore exception: "+e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.core.resources.IProject;
|
||||
import org.eclipse.core.runtime.IProgressMonitor;
|
||||
import org.eclipse.core.runtime.IStatus;
|
||||
import org.eclipse.core.runtime.Status;
|
||||
@@ -54,6 +55,26 @@ public class ReusableClasspathListenerHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean projectExists(IJavaProject jp) {
|
||||
//We can't really deal with projects that don't exist in disk. So using this more strict 'exists' check
|
||||
//makes sure anything that looks like it doesn't exist on disk is treated as if it simply doesn't exist
|
||||
//at all. This kind of addresses a issue caused by Eclipse's idiotic behavior when it comes to deleting
|
||||
//a project's files from the file system... Eclipse recreates a 'vanilla' project in the workspace,
|
||||
//simply refusing to accept the fact that the project is actually gone.
|
||||
if (jp.exists()) {
|
||||
try {
|
||||
URI loc = getProjectLocation(jp);
|
||||
if (loc!=null) {
|
||||
File f = new File(loc);
|
||||
return f.isDirectory();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//Something bogus about this project... so just pretend it doesn't exist.
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
class Subscribptions {
|
||||
|
||||
@@ -86,7 +107,7 @@ public class ReusableClasspathListenerHandler {
|
||||
if (projectLoc==null) {
|
||||
Logger.log("Could not send event for project because no project location: "+jp.getElementName());
|
||||
} else {
|
||||
boolean exsits = jp.exists();
|
||||
boolean exsits = projectExists(jp);
|
||||
boolean open = true; // WARNING: calling is jp.isOpen is unreliable and subject to race condition. After a POST_CHAGE project open event
|
||||
// this should be true but it typically is not unless you wait for some time. No idea how you would know
|
||||
// how long you should wait (200ms is not enough, and that seems pretty long). Isn't it kind of the point
|
||||
|
||||
@@ -220,12 +220,12 @@
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.block.scalar.literal.yaml</string>
|
||||
<string>keyword.control.flow.block-scalar.literal.yaml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.block.scalar.folded.yaml</string>
|
||||
<string>keyword.control.flow.block-scalar.folded.yaml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
@@ -235,7 +235,7 @@
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>support.other.chomping-indicator.yaml</string>
|
||||
<string>storage.modifier.chomping-indicator.yaml</string>
|
||||
</dict>
|
||||
<key>5</key>
|
||||
<dict>
|
||||
@@ -271,7 +271,7 @@
|
||||
<key>block-sequence</key>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(-)( |\t|$)</string>
|
||||
<string>(-)(?!\S)</string>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.block.sequence.item.yaml</string>
|
||||
</dict>
|
||||
@@ -370,8 +370,8 @@
|
||||
(?:[ \t]+
|
||||
((?:!(?:[0-9A-Za-z\-]*!)?))
|
||||
(?:[ \t]+ (
|
||||
! (?x: %\p{XDigit}{2} | [0-9A-Za-z\-#;/?:@&=+$,_.!~*'()\[\]] )*
|
||||
| (?![,!\[\]{}]) (?x: %\p{XDigit}{2} | [0-9A-Za-z\-#;/?:@&=+$,_.!~*'()\[\]] )+
|
||||
! (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\-#;/?:@&=+$,_.!~*'()\[\]] )*
|
||||
| (?![,!\[\]{}]) (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\-#;/?:@&=+$,_.!~*'()\[\]] )+
|
||||
)
|
||||
)?
|
||||
)?
|
||||
@@ -1124,8 +1124,8 @@
|
||||
<string>(?x)
|
||||
\G
|
||||
(?:
|
||||
! < (?: %\p{XDigit}{2} | [0-9A-Za-z\-#;/?:@&=+$,_.!~*'()\[\]] )+ >
|
||||
| (?:!(?:[0-9A-Za-z\-]*!)?) (?: %\p{XDigit}{2} | [0-9A-Za-z\-#;/?:@&=+$_.~*'()] )+
|
||||
! < (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\-#;/?:@&=+$,_.!~*'()\[\]] )+ >
|
||||
| (?:!(?:[0-9A-Za-z\-]*!)?) (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\-#;/?:@&=+$_.~*'()] )+
|
||||
| !
|
||||
)
|
||||
(?=\ |\t|$)
|
||||
|
||||
Reference in New Issue
Block a user