Bug 510494 - [Test] test*ProjectExplicitPath failed to write on Windows

Reverted asynchronous monitor/unmonitor method calls added via commit
f2152ad44481651f5e43066e6f184562de9cb2a3 for bug 501306.

Those calls make installing or uninstaling monitors run asynchronously
with other file system operations triggered for same resources,
resulting in obscure random java.nio.file.AccessDeniedException on
Windows.

Added regression test RefreshProviderTest.testProjectCreateDelete()
which simply executes subsequent create/delete operations and counts
failures. This failures occur without the fix in average 3 from 1000
times, with fix the test do not fail anymore.

Change-Id: Ie55cddc63cea3a7e5ebab8762f659d19afe4ad78
Signed-off-by: Andrey Loskutov <loskutov@gmx.de>
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/refresh/MonitorManager.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/refresh/MonitorManager.java
index b7206f2..2a31a12 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/refresh/MonitorManager.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/refresh/MonitorManager.java
@@ -22,7 +22,6 @@
 import org.eclipse.core.resources.refresh.IRefreshMonitor;
 import org.eclipse.core.resources.refresh.RefreshProvider;
 import org.eclipse.core.runtime.*;
-import org.eclipse.osgi.util.NLS;
 
 /**
  * Manages monitors by creating new monitors when projects are added and
@@ -123,7 +122,7 @@
 			case LifecycleEvent.PRE_LINK_DELETE :
 			case LifecycleEvent.PRE_PROJECT_CLOSE :
 			case LifecycleEvent.PRE_PROJECT_DELETE :
-				unmonitorAsync(event.resource);
+				unmonitor(event.resource, new NullProgressMonitor());
 				break;
 		}
 	}
@@ -366,34 +365,14 @@
 		if (delta.getKind() == IResourceDelta.ADDED) {
 			IResource resource = delta.getResource();
 			if (resource.isLinked())
-				monitorAsync(resource);
+				monitor(resource, new NullProgressMonitor());
 		}
 		if ((delta.getFlags() & IResourceDelta.OPEN) != 0) {
 			IProject project = (IProject) delta.getResource();
 			if (project.isAccessible())
-				monitorAsync(project);
+				monitor(project, new NullProgressMonitor());
 		}
 		return true;
 	}
 
-	private void monitorAsync(final IResource resource) {
-		MonitorJob.createSystem(NLS.bind(Messages.refresh_installMonitor, resource), resource, new ICoreRunnable() {
-			@Override
-			public void run(IProgressMonitor monitor) {
-				monitor(resource, monitor);
-				// Because the monitor is installed asynchronously we
-				// may have missed some changes, we need to refresh it.
-				refreshManager.refresh(resource);
-			}
-		}).schedule();
-	}
-
-	private void unmonitorAsync(final IResource resource) {
-		MonitorJob.createSystem(NLS.bind(Messages.refresh_uninstallMonitor, resource), resource, new ICoreRunnable() {
-			@Override
-			public void run(IProgressMonitor monitor) {
-				unmonitor(resource, monitor);
-			}
-		}).schedule();
-	}
 }
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java
index 4e3cb16..34e8a2b 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java
@@ -150,14 +150,12 @@
 
 	// auto-refresh
 	public static String refresh_installError;
-	public static String refresh_installMonitor;
 	public static String refresh_installMonitorsOnWorkspace;
 	public static String refresh_jobName;
 	public static String refresh_pollJob;
 	public static String refresh_refreshErr;
 	public static String refresh_restoreOnInvalid;
 	public static String refresh_task;
-	public static String refresh_uninstallMonitor;
 	public static String refresh_uninstallMonitorsOnWorkspace;
 
 	public static String resources_cannotModify;
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties
index 978ec63..eadd09c 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties
@@ -154,9 +154,7 @@
 refresh_refreshErr = Problems occurred while refreshing local changes
 refresh_restoreOnInvalid = Restore auto-refresh monitors on invalid resources
 refresh_installError = An error occurred while installing an auto-refresh monitor
-refresh_installMonitor = Installing auto-refresh monitor on ''{0}''
 refresh_installMonitorsOnWorkspace = Installing auto-refresh monitors on the workspace
-refresh_uninstallMonitor = Uninstalling auto-refresh monitor from ''{0}''
 refresh_uninstallMonitorsOnWorkspace = Uninstalling auto-refresh monitors from the workspace
 
 resources_cannotModify = The resource tree is locked for modifications.
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/TestUtil.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/TestUtil.java
new file mode 100644
index 0000000..f750a5d
--- /dev/null
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/TestUtil.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Google, Inc and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *   Stefan Xenos (Google) - Initial implementation
+ *   Andrey Loskutov <loskutov@gmx.de> - more logging
+ *******************************************************************************/
+package org.eclipse.core.tests.resources;
+
+import java.util.*;
+import org.eclipse.core.internal.runtime.InternalPlatform;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.Job;
+import org.junit.Assert;
+
+public class TestUtil {
+
+	/**
+	 * Call this in the tearDown method of every test to clean up state that can
+	 * otherwise leak through SWT between tests.
+	 */
+	public static void cleanUp(String owner) {
+		// Ensure that the Thread.interrupted() flag didn't leak.
+		Assert.assertFalse("The main thread should not be interrupted at the end of a test", Thread.interrupted());
+
+		// Wait for any outstanding jobs to finish. Protect against deadlock by
+		// terminating the wait after a timeout.
+		boolean timedOut = waitForJobs(owner, 5, 5000);
+		if (timedOut) {
+			// We don't expect any extra jobs run during the test: try to cancel them
+			log(IStatus.INFO, owner, "Trying to cancel running jobs: " + getRunningOrWaitingJobs(null));
+			getRunningOrWaitingJobs(null).forEach(job -> job.cancel());
+			waitForJobs(owner, 5, 1000);
+		}
+
+		// Ensure that the Thread.interrupted() flag didn't leak.
+		Assert.assertFalse("The main thread should not be interrupted at the end of a test", Thread.interrupted());
+	}
+
+	public static void log(int severity, String owner, String message, Throwable... optionalError) {
+		message = "[" + owner + "] " + message;
+		Throwable error = null;
+		if (optionalError != null && optionalError.length > 0) {
+			error = optionalError[0];
+		}
+		Status status = new Status(severity, "org.eclipse.core.tests.resources", message, error);
+		InternalPlatform.getDefault().getLog(Platform.getBundle("org.eclipse.core.tests.resources")).log(status);
+	}
+
+	/**
+	 * Utility for waiting until the execution of jobs of any family has finished or timeout is reached. If no jobs are running, the method waits
+	 * given minimum wait time. While this method is waiting for jobs, UI events are processed.
+	 *
+	 * @param owner
+	 *            name of the caller which will be logged as prefix if the wait times out
+	 * @param minTimeMs
+	 *            minimum wait time in milliseconds
+	 * @param maxTimeMs
+	 *            maximum wait time in milliseconds
+	 * @return true if the method timed out, false if all the jobs terminated before the timeout
+	 */
+	public static boolean waitForJobs(String owner, long minTimeMs, long maxTimeMs) {
+		if (maxTimeMs < minTimeMs) {
+			throw new IllegalArgumentException("Max time is smaller as min time!");
+		}
+		final long start = System.currentTimeMillis();
+		while (System.currentTimeMillis() - start < minTimeMs) {
+			try {
+				Thread.sleep(Math.min(10, minTimeMs));
+			} catch (InterruptedException e) {
+				// Uninterruptable
+			}
+		}
+		while (!Job.getJobManager().isIdle()) {
+			List<Job> jobs = getRunningOrWaitingJobs((Object) null);
+
+			if (!Collections.disjoint(runningJobs, jobs)) {
+				// There is a job which runs already quite some time, don't wait for it to avoid test timeouts
+				dumpRunningOrWaitingJobs(owner, jobs);
+				return true;
+			}
+
+			if (System.currentTimeMillis() - start >= maxTimeMs) {
+				dumpRunningOrWaitingJobs(owner, jobs);
+				return true;
+			}
+			try {
+				Thread.sleep(10);
+			} catch (InterruptedException e) {
+				// Uninterruptable
+			}
+		}
+		runningJobs.clear();
+		return false;
+	}
+
+	static Set<Job> runningJobs = new LinkedHashSet<>();
+
+	private static void dumpRunningOrWaitingJobs(String owner, List<Job> jobs) {
+		String message = "Some job is still running or waiting to run: " + dumpRunningOrWaitingJobs(jobs);
+		log(IStatus.ERROR, owner, message);
+	}
+
+	private static String dumpRunningOrWaitingJobs(List<Job> jobs) {
+		if (jobs.isEmpty()) {
+			return "";
+		}
+		// clear "old" running jobs, we only remember most recent
+		runningJobs.clear();
+		StringBuilder sb = new StringBuilder();
+		for (Job job : jobs) {
+			runningJobs.add(job);
+			sb.append("'").append(job.getName()).append("'/");
+			sb.append(job.getClass().getName());
+			sb.append(", ");
+		}
+		sb.setLength(sb.length() - 2);
+		return sb.toString();
+	}
+
+	private static List<Job> getRunningOrWaitingJobs(Object jobFamily) {
+		List<Job> running = new ArrayList<>();
+		Job[] jobs = Job.getJobManager().find(jobFamily);
+		for (Job job : jobs) {
+			if (isRunningOrWaitingJob(job)) {
+				running.add(job);
+			}
+		}
+		return running;
+	}
+
+	private static boolean isRunningOrWaitingJob(Job job) {
+		int state = job.getState();
+		return (state == Job.RUNNING || state == Job.WAITING);
+	}
+
+}
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/refresh/RefreshProviderTest.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/refresh/RefreshProviderTest.java
index 11919db..bfffc05 100644
--- a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/refresh/RefreshProviderTest.java
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/refresh/RefreshProviderTest.java
@@ -10,6 +10,8 @@
  *******************************************************************************/
 package org.eclipse.core.tests.resources.refresh;
 
+import java.util.HashMap;
+import java.util.Map;
 import junit.framework.AssertionFailedError;
 import junit.framework.TestSuite;
 import org.eclipse.core.internal.resources.Workspace;
@@ -17,6 +19,7 @@
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.core.tests.resources.ResourceTest;
+import org.eclipse.core.tests.resources.TestUtil;
 
 /**
  * Tests the IRefreshMonitor interface
@@ -64,10 +67,11 @@
 	 * Test to ensure that a refresh provider is given the correct events when a linked
 	 * file is created and deleted.
 	 */
-	public void testLinkedFile() throws InterruptedException {
+	public void testLinkedFile() throws Exception {
 		IPath location = getRandomLocation();
+		String name = "testUnmonitorLinkedResource";
 		try {
-			IProject project = getWorkspace().getRoot().getProject("testUnmonitorLinkedResource");
+			IProject project = getWorkspace().getRoot().getProject(name);
 			ensureExistsInWorkspace(project, true);
 			joinAutoRefreshJobs();
 			IFile link = project.getFile("Link");
@@ -92,6 +96,7 @@
 		} finally {
 			//cleanup
 			Workspace.clear(location.toFile());
+			deleteProject(name);
 		}
 	}
 
@@ -99,9 +104,10 @@
 	 * Test to ensure that a refresh provider is given the correct events when a project
 	 * is closed or opened.
 	 */
-	public void testProjectCloseOpen() throws InterruptedException {
+	public void testProjectCloseOpen() throws Exception {
+		String name = "testProjectCloseOpen";
+		IProject project = getWorkspace().getRoot().getProject(name);
 		try {
-			IProject project = getWorkspace().getRoot().getProject("testProjectCloseOpen");
 			ensureExistsInWorkspace(project, true);
 			joinAutoRefreshJobs();
 			//ensure we currently have just the project being monitored
@@ -122,9 +128,66 @@
 				fail("" + failures.length + " failures", failures[0]);
 		} catch (CoreException e) {
 			fail("1.99", e);
+		} finally {
+			deleteProject(name);
 		}
 	}
 
+	/**
+	 * Test to ensure that a refresh provider is given the correct events when a project
+	 * is closed or opened.
+	 */
+	public void testProjectCreateDelete() throws Exception {
+		String name = "testProjectCreateDelete";
+		final int maxRuns = 1000;
+		int i = 0;
+		Map<Integer, Throwable> fails = new HashMap<>();
+		try {
+			for (; i < maxRuns; i++) {
+				if (i % 50 == 0) {
+					TestUtil.waitForJobs(getName(), 5, 100);
+				}
+				try {
+					assertTrue(createProject(name).isAccessible());
+					assertFalse(deleteProject(name).exists());
+				} catch (CoreException e) {
+					fails.put(i, e);
+				}
+			}
+		} finally {
+			deleteProject(name);
+		}
+		if (!fails.isEmpty()) {
+			fail("Failed " + fails.size() + " times out of " + i, fails.values().iterator().next());
+		}
+	}
+
+	private IProject createProject(String name) throws Exception {
+		IProject pro = ResourcesPlugin.getWorkspace().getRoot().getProject(name);
+		if (pro.exists()) {
+			pro.delete(true, true, null);
+		}
+		IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+		IProject project = root.getProject(name);
+		if (!project.exists()) {
+			project.create(null);
+		} else {
+			project.refreshLocal(IResource.DEPTH_INFINITE, null);
+		}
+		if (!project.isOpen()) {
+			project.open(null);
+		}
+		return project;
+	}
+
+	private static IProject deleteProject(String name) throws Exception {
+		IProject pro = ResourcesPlugin.getWorkspace().getRoot().getProject(name);
+		if (pro.exists()) {
+			pro.delete(true, true, null);
+		}
+		return pro;
+	}
+
 	private void joinAutoRefreshJobs() throws InterruptedException {
 		// We must join on the auto-refresh family because the workspace changes done in the 
 		// tests above may be batched and broadcasted by the RefreshJob, not the main thread.