Bug 185247 - JUnit tests for recursive symlinks

Test cases attached to Bug 105554 now added as plug-in tests.

Change-Id: Ib44cdc39e8f9620656349d191d51bba5107d7ed5
diff --git a/tests/org.eclipse.core.tests.resources/build.properties b/tests/org.eclipse.core.tests.resources/build.properties
index ce7b138..c8ae13c 100644
--- a/tests/org.eclipse.core.tests.resources/build.properties
+++ b/tests/org.eclipse.core.tests.resources/build.properties
@@ -19,7 +19,8 @@
                about.html,\
                META-INF/,\
                test.xml,\
-               Plugin_Testing/
+               Plugin_Testing/,\
+               resources/
 src.includes = about.html
 javacWarnings.resourcestests.jar=-raw,-unchecked
 
diff --git a/tests/org.eclipse.core.tests.resources/resources/bug185247/bug185247_LinuxTests.zip b/tests/org.eclipse.core.tests.resources/resources/bug185247/bug185247_LinuxTests.zip
new file mode 100644
index 0000000..8fc1673
--- /dev/null
+++ b/tests/org.eclipse.core.tests.resources/resources/bug185247/bug185247_LinuxTests.zip
Binary files differ
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/AllTests.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/AllTests.java
index 21ef3a7..1dad556 100644
--- a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/AllTests.java
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/AllTests.java
@@ -81,6 +81,8 @@
 		suite.addTest(PR_1GH2B0N_Test.suite());
 		suite.addTest(PR_1GHOM0N_Test.suite());
 		suite.addTest(Bug_530868.suite());
+		suite.addTest(Bug_185247_recursiveLinks.suite());
+		suite.addTest(Bug_185247_LinuxTests.suite());
 		return suite;
 	}
 }
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/Bug_185247_LinuxTests.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/Bug_185247_LinuxTests.java
new file mode 100644
index 0000000..f9d894e
--- /dev/null
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/Bug_185247_LinuxTests.java
@@ -0,0 +1,169 @@
+/*******************************************************************************
+ *  Copyright (c) 2018 Simeon Andreev 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:
+ *     Simeon Andreev - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.core.tests.resources.regression;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+import org.eclipse.core.internal.resources.ProjectDescription;
+import org.eclipse.core.resources.*;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.tests.resources.ResourceTest;
+
+
+/**
+ * Test cases for symbolic links in projects.
+ */
+public class Bug_185247_LinuxTests extends ResourceTest {
+
+	private static final boolean IS_LINUX = Platform.getOS().equals(Platform.OS_LINUX);
+	private final List<IProject> testProjects = new ArrayList<>();
+	private IPath testCasesLocation;
+
+	public static Test suite() {
+		return new TestSuite(Bug_185247_LinuxTests.class);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		IPath randomLocation = getRandomLocation();
+		deleteOnTearDown(randomLocation);
+		testCasesLocation = randomLocation.append("bug185247LinuxTests");
+		assertTrue("failed to create test location: " + testCasesLocation, testCasesLocation.toFile().mkdirs());
+		extractTestCasesArchive(testCasesLocation);
+	}
+
+	private void extractTestCasesArchive(IPath outputLocation) throws Exception {
+		if (IS_LINUX) {
+			URL testCasesArchive = Platform.getBundle("org.eclipse.core.tests.resources")
+					.getEntry("resources/bug185247/bug185247_LinuxTests.zip");
+			URL archiveLocation = FileLocator.resolve(testCasesArchive);
+			File archive = URIUtil.toFile(archiveLocation.toURI());
+			assertNotNull("cannot find archive with test cases", archive);
+			unzip(archive, outputLocation.toFile());
+		}
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		try {
+			for (IProject testProject : testProjects) {
+				testProject.delete(false, true, getMonitor());
+			}
+		} finally {
+			super.tearDown();
+		}
+	}
+
+	/**
+	 *
+	 */
+	public void test1_trivial() throws Exception {
+		runProjectTestCase();
+	}
+
+	/**
+	 *
+	 */
+	public void test2_mutual() throws Exception {
+		runProjectTestCase();
+	}
+
+	/**
+	 *
+	 */
+	public void test3_outside_tree() throws Exception {
+		runProjectTestCase();
+	}
+
+	/**
+	 *
+	 */
+	public void test5_transitive_mutual() throws Exception {
+		runProjectTestCase();
+	}
+
+	/**
+	 *
+	 */
+	public void test6_nonrecursive() throws Exception {
+		runProjectTestCase();
+	}
+
+	private void runProjectTestCase() throws Exception {
+		String projectName = getName();
+		// refresh should hang, if bug 105554 re-occurs
+		importProjectAndRefresh(projectName);
+	}
+
+	private void importProjectAndRefresh(String projectName) throws Exception {
+		if (IS_LINUX) {
+			IProject project = importTestProject(projectName);
+			project.refreshLocal(IResource.DEPTH_INFINITE, getMonitor());
+		}
+	}
+
+	private IProject importTestProject(String projectName) throws Exception {
+		IProject testProject = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
+		testProjects.add(testProject);
+		IProjectDescription projectDescription = new ProjectDescription();
+		projectDescription.setName(projectName);
+		String projectRoot = String.join(File.separator, testCasesLocation.toOSString(), "bug185247", projectName);
+		projectDescription.setLocationURI(URI.create(projectRoot));
+		testProject.create(projectDescription, getMonitor());
+		testProject.open(getMonitor());
+		assertTrue("expected project to be open: " + projectName, testProject.isAccessible());
+		return testProject;
+	}
+
+	private static void unzip(File archive, File outputDirectory) throws Exception {
+		String[] command = { "unzip", archive.toString(), "-d", outputDirectory.toString() };
+		executeCommand(command, outputDirectory);
+
+	}
+
+	private static void executeCommand(String[] command, File outputDirectory) throws Exception {
+		assertTrue("output directory does not exist: " + outputDirectory, outputDirectory.exists());
+		assertTrue("commands only availabe in Linux environment", IS_LINUX);
+		ProcessBuilder processBuilder = new ProcessBuilder(command);
+		File commandOutputFile = new File(outputDirectory, "commandOutput.txt");
+		if (!commandOutputFile.exists()) {
+			assertTrue("failed to create standard output and error file for unzip command",
+					commandOutputFile.createNewFile());
+		}
+		processBuilder.redirectOutput(commandOutputFile);
+		processBuilder.redirectError(commandOutputFile);
+		Process process = processBuilder.start();
+		int commandExitCode = process.waitFor();
+		String output = formatCommandOutput(command, commandOutputFile);
+		assertTrue("Failed to delete command output file. " + output, commandOutputFile.delete());
+		assertEquals("Failed to execute commmand. " + output, 0, commandExitCode);
+	}
+
+	private static String formatCommandOutput(String[] command, File commandOutputFile) throws IOException {
+		Path commandOutputPath = Paths.get(commandOutputFile.getAbsolutePath());
+		List<String> commandOutputLines = Files.readAllLines(commandOutputPath);
+		List<String> commandOutputHeader = Arrays.asList("Command:", Arrays.toString(command), "Output:");
+		List<String> commandToString = new ArrayList<>();
+		commandToString.addAll(commandOutputHeader);
+		commandToString.addAll(commandOutputLines);
+		String formattedOutput = String.join(System.lineSeparator(), commandToString);
+		return formattedOutput;
+	}
+}
\ No newline at end of file
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/Bug_185247_recursiveLinks.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/Bug_185247_recursiveLinks.java
new file mode 100644
index 0000000..b7444aa
--- /dev/null
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/Bug_185247_recursiveLinks.java
@@ -0,0 +1,233 @@
+/*******************************************************************************
+ *  Copyright (c) 2018 Simeon Andreev 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:
+ *     Simeon Andreev - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.core.tests.resources.regression;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+import org.eclipse.core.filesystem.URIUtil;
+import org.eclipse.core.internal.resources.ProjectDescription;
+import org.eclipse.core.resources.*;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.tests.resources.ResourceTest;
+
+
+/**
+ * Tests for recursive symbolic links in projects.
+ */
+public class Bug_185247_recursiveLinks extends ResourceTest {
+
+	private final List<IProject> testProjects = new ArrayList<>();
+
+	public static Test suite() {
+		return new TestSuite(Bug_185247_recursiveLinks.class);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		try {
+			cleanUpTestProjects();
+		} finally {
+			super.tearDown();
+		}
+	}
+
+	private void cleanUpTestProjects() throws CoreException {
+		for (IProject testProject : testProjects) {
+			testProject.delete(false, true, getMonitor());
+		}
+	}
+
+	/**
+	 * Test project structure:
+	 *
+	 * <pre>
+	 * project root
+	 *   |
+	 *   |-- directory
+	 *         |
+	 *         |-- link_current -> ./ (links "directory")
+	 * </pre>
+	 */
+	public void test1_linkCurrentDirectory() throws Exception {
+		CreateTestProjectStructure createSymlinks = directory -> {
+			createSymlink(directory, "link_current", "./");
+		};
+
+		runTest(createSymlinks);
+	}
+
+	/**
+	 * Test project structure:
+	 *
+	 * <pre>
+	 * project root
+	 *   |
+	 *   |-- directory
+	 *         |
+	 *         |-- link_parent -> ../ (links "project root")
+	 * </pre>
+	 */
+	public void test2_linkParentDirectory() throws Exception {
+		CreateTestProjectStructure createSymlinks = directory -> {
+			createSymlink(directory, "link_parent", "../");
+		};
+
+		runTest(createSymlinks);
+	}
+
+	/**
+	 * Test project structure:
+	 *
+	 * <pre>
+	 * project root
+	 *   |
+	 *   |-- directory
+	 *         |
+	 *         |-- subdirectory
+	 *              |
+	 *              |-- link_grandparent -> ../../ (links "project root")
+	 * </pre>
+	 */
+	public void test3_linkGrandparentDirectory() throws Exception {
+		CreateTestProjectStructure createSymlinks = directory -> {
+			File subdirectory = new File(directory, "subdirectory");
+			createDirectory(subdirectory);
+			createSymlink(subdirectory, "link_grandparent", "../../");
+		};
+
+		runTest(createSymlinks);
+	}
+
+	/**
+	 * Test project structure:
+	 *
+	 * <pre>
+	 * project root
+	 *   |
+	 *   |-- directory
+	 *         |
+	 *         |-- subdirectory1
+	 *         |    |
+	 *         |    |-- link_parent -> ../ (links directory)
+	 *         |
+	 *         |-- subdirectory2
+	 *              |
+	 *              |-- link_parent -> ../ (links directory)
+	 * </pre>
+	 */
+	public void test4_linkParentDirectoryTwice() throws Exception {
+		CreateTestProjectStructure createSymlinks = directory -> {
+			String[] subdirectoryNames = { "subdirectory1", "subdirectory2" };
+			for (String subdirectoryName : subdirectoryNames) {
+				File subdirectory = new File(directory, subdirectoryName);
+				createDirectory(subdirectory);
+				createSymlink(subdirectory, "link_parent", "../../");
+			}
+		};
+
+		runTest(createSymlinks);
+	}
+
+	/**
+	 * Test project structure:
+	 *
+	 * <pre>
+	 * project root
+	 *   |
+	 *   |-- directory
+	 *         |
+	 *         |-- subdirectory1
+	 *         |    |
+	 *         |    |-- link_parent -> /tmp/<random string>/bug185247recursive/test5_linkParentDirectoyTwiceWithAbsolutePath/directory
+	 *         |
+	 *         |-- subdirectory2
+	 *              |
+	 *              |-- link_parent -> /tmp/<random string>/bug185247recursive/test5_linkParentDirectoyTwiceWithAbsolutePath/directory
+	 * </pre>
+	 */
+	public void test5_linkParentDirectoyTwiceWithAbsolutePath() throws Exception {
+		CreateTestProjectStructure createSymlinks = directory -> {
+			String[] subdirectoryNames = { "subdirectory1", "subdirectory2" };
+			for (String subdirectoryName : subdirectoryNames) {
+				File subdirectory = new File(directory, subdirectoryName);
+				createDirectory(subdirectory);
+				createSymlink(subdirectory, "link_parent", directory.getAbsolutePath());
+			}
+		};
+
+		runTest(createSymlinks);
+	}
+
+	private void runTest(CreateTestProjectStructure createSymlinks) throws MalformedURLException, Exception {
+		if (!canCreateSymLinks()) {
+			/*
+			 * we don't run this test case on platforms that have no symlinks, since we want
+			 * to test recursive symlinks
+			 */
+			return;
+		}
+
+		String projectName = getName();
+		IPath testRoot = getRandomLocation();
+		deleteOnTearDown(testRoot);
+		IPath projectRoot = testRoot.append("bug185247recursive").append(projectName);
+		File directory = projectRoot.append("directory").toFile();
+		createDirectory(directory);
+
+		createSymlinks.accept(directory);
+
+
+		URI projectRootLocation = URIUtil.toURI((projectRoot));
+		// refreshing the project with recursive symlinks should not hang
+		importProjectAndRefresh(projectName, projectRootLocation);
+	}
+
+	private static void createDirectory(File directory) {
+		assertTrue("failed to create test directory: " + directory, directory.mkdirs());
+	}
+
+	void createSymlink(File directory, String linkName, String linkTarget) {
+		assertTrue("symlinks not supported by platform", canCreateSymLinks());
+		boolean isDir = true;
+		createSymLink(directory, linkName, linkTarget, isDir);
+	}
+
+	private void importProjectAndRefresh(String projectName, URI projectRootLocation) throws Exception {
+		IProject project = importTestProject(projectName, projectRootLocation);
+		project.refreshLocal(IResource.DEPTH_INFINITE, getMonitor());
+	}
+
+	private IProject importTestProject(String projectName, URI projectRootLocation) throws Exception {
+		IProject testProject = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
+		testProjects.add(testProject);
+		IProjectDescription projectDescription = new ProjectDescription();
+		projectDescription.setName(projectName);
+		projectDescription.setLocationURI(projectRootLocation);
+		testProject.create(projectDescription, getMonitor());
+		testProject.open(getMonitor());
+		assertTrue("expected project to be open: " + projectName, testProject.isAccessible());
+		return testProject;
+	}
+
+	interface CreateTestProjectStructure extends Consumer<File> {
+		/*
+		 * we give a class name for the runnable that we use to create different project
+		 * structures in different tests.
+		 */
+	}
+}