| /******************************************************************************* |
| * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> |
| * Copyright (C) 2010, Jens Baumgart <jens.baumgart@sap.com> |
| * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com> |
| * Copyright (C) 2012, François Rey <eclipse.org_@_francois_._rey_._name> |
| * |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| *******************************************************************************/ |
| package org.eclipse.egit.core.test; |
| |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.lang.management.LockInfo; |
| import java.lang.management.ManagementFactory; |
| import java.lang.management.ThreadInfo; |
| import java.lang.management.ThreadMXBean; |
| import java.nio.file.DirectoryNotEmptyException; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.UUID; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IFolder; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IProjectDescription; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.ICoreRunnable; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.core.runtime.jobs.IJobManager; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.treewalk.TreeWalk; |
| import org.eclipse.jgit.util.FS; |
| import org.eclipse.jgit.util.FileUtils; |
| |
| public class TestUtils { |
| |
| public final static String AUTHOR = "The Author <The.author@some.com>"; |
| |
| public final static String COMMITTER = "The Commiter <The.committer@some.com>"; |
| |
| private final static File rootDir = customTestDirectory(); |
| |
| private static final int MAX_DELETE_RETRY = 5; |
| |
| private static final int DELETE_RETRY_DELAY = 1000; // ms |
| |
| /** |
| * Allow to set a custom directory for running tests |
| * |
| * @return custom directory defined by system property |
| * {@code egit.test.tmpdir} or {@code ~/egit.test.tmpdir} if this |
| * property isn't defined |
| */ |
| private static File customTestDirectory() { |
| final String p = System.getProperty("egit.test.tmpdir"); //$NON-NLS-1$ |
| File testDir = null; |
| boolean isDefault = true; |
| if (p == null || p.length() == 0) |
| testDir = new File(FS.DETECTED.userHome(), "egit.test.tmpdir"); //$NON-NLS-1$ |
| else { |
| isDefault = false; |
| testDir = new File(p).getAbsoluteFile(); |
| } |
| System.out.println("egit.test.tmpdir" //$NON-NLS-1$ |
| + (isDefault ? "[default]: " : ": ") //$NON-NLS-1$ $NON-NLS-2$ |
| + testDir.getAbsolutePath()); |
| return testDir; |
| } |
| |
| private File baseTempDir; |
| |
| public TestUtils() { |
| // ensure that concurrent test runs don't use the same directory |
| baseTempDir = new File(rootDir, UUID.randomUUID().toString() |
| .replace("-", "")); |
| } |
| |
| /** |
| * Return the base directory in which temporary directories are created. |
| * Current implementation returns a "temporary" folder in the user home. |
| * |
| * @return a "temporary" folder in the user home that may not exist. |
| */ |
| public File getBaseTempDir() { |
| return baseTempDir; |
| } |
| |
| /** |
| * Create a "temporary" directory |
| * |
| * @param name |
| * the name of the directory |
| * @return a directory as child of a "temporary" folder in the user home |
| * directory; may or may not exist |
| * @throws IOException |
| */ |
| public File createTempDir(String name) throws IOException { |
| File result = new File(getBaseTempDir(), name); |
| if (result.exists()) |
| FileUtils.delete(result, FileUtils.RECURSIVE | FileUtils.RETRY); |
| FileUtils.mkdirs(result, true); |
| return result; |
| } |
| |
| /** |
| * Cleanup: delete the "temporary" folder and all children |
| * |
| * @throws IOException |
| */ |
| public void deleteTempDirs() throws IOException { |
| if (rootDir.exists()) { |
| try { |
| FileUtils.delete(rootDir, |
| FileUtils.RECURSIVE | FileUtils.RETRY); |
| } catch (DirectoryNotEmptyException e) { |
| System.err.println(e.toString()); |
| listDirectory(rootDir, true); |
| throw e; |
| } |
| } |
| } |
| |
| /** |
| * Produce a simple directory listing. |
| * |
| * @param directory |
| * to list |
| * @param recursive |
| * whether to descend into sub-directories |
| */ |
| public static void listDirectory(File directory, boolean recursive) { |
| try { |
| java.nio.file.Path top = directory.toPath(); |
| Files.walkFileTree(top, |
| new SimpleFileVisitor<java.nio.file.Path>() { |
| |
| private void print(java.nio.file.Path path, |
| BasicFileAttributes attrs) { |
| StringBuilder b = new StringBuilder(); |
| b.append(attrs.lastModifiedTime().toString()); |
| b.append(' '); |
| b.append(path.toString()); |
| if (attrs.isSymbolicLink()) { |
| b.append(" (symlink)"); |
| } else if (attrs.isDirectory()) { |
| b.append('/'); |
| } |
| System.out.println(b.toString()); |
| } |
| |
| @Override |
| public FileVisitResult preVisitDirectory( |
| java.nio.file.Path dir, |
| BasicFileAttributes attrs) throws IOException { |
| print(dir, attrs); |
| return (recursive || top.equals(dir)) |
| ? FileVisitResult.CONTINUE |
| : FileVisitResult.SKIP_SUBTREE; |
| } |
| |
| @Override |
| public FileVisitResult visitFile( |
| java.nio.file.Path file, |
| BasicFileAttributes attrs) throws IOException { |
| print(file, attrs); |
| return FileVisitResult.CONTINUE; |
| } |
| }); |
| } catch (Exception e) { |
| System.err.println("[ERROR] Error listing directory: " + directory); |
| e.printStackTrace(); |
| } |
| } |
| |
| /** |
| * Read the stream into a String |
| * |
| * @param inputStream |
| * @return the contents of the stream |
| * @throws IOException |
| */ |
| public String slurpAndClose(InputStream inputStream) throws IOException { |
| StringBuilder stringBuilder = new StringBuilder(); |
| try { |
| int ch; |
| while ((ch = inputStream.read()) != -1) { |
| stringBuilder.append((char) ch); |
| } |
| } finally { |
| inputStream.close(); |
| } |
| return stringBuilder.toString(); |
| } |
| |
| /** |
| * Add a file to an existing project |
| * |
| * @param project |
| * the project |
| * @param path |
| * e.g. "folder1/folder2/test.txt" |
| * @param content |
| * the contents |
| * @return the file |
| * @throws CoreException |
| * if the file cannot be created |
| * @throws UnsupportedEncodingException |
| */ |
| public IFile addFileToProject(IProject project, String path, String content) throws CoreException, UnsupportedEncodingException { |
| IPath filePath = new Path(path); |
| IFolder folder = null; |
| for (int i = 0; i < filePath.segmentCount() - 1; i++) { |
| if (folder == null) { |
| folder = project.getFolder(filePath.segment(i)); |
| } else { |
| folder = folder.getFolder(filePath.segment(i)); |
| } |
| if (!folder.exists()) |
| folder.create(false, true, null); |
| } |
| IFile file = project.getFile(filePath); |
| file.create(new ByteArrayInputStream(content.getBytes(project |
| .getDefaultCharset())), true, null); |
| return file; |
| } |
| |
| /** |
| * Change the content of a file |
| * |
| * @param project |
| * @param file |
| * @param newContent |
| * @return the file |
| * @throws CoreException |
| * @throws UnsupportedEncodingException |
| */ |
| public IFile changeContentOfFile(IProject project, IFile file, String newContent) throws UnsupportedEncodingException, CoreException { |
| file.setContents(new ByteArrayInputStream(newContent.getBytes(project |
| .getDefaultCharset())), 0, null); |
| return file; |
| } |
| |
| /** |
| * Create a project in the base directory of temp dirs |
| * |
| * @param projectName |
| * project name |
| * @return the project with a location pointing to the local file system |
| * @throws Exception |
| */ |
| public IProject createProjectInLocalFileSystem( |
| String projectName) throws Exception { |
| return createProjectInLocalFileSystem(getBaseTempDir(), projectName); |
| } |
| |
| /** |
| * Create a project in the local file system |
| * |
| * @param parentFile |
| * the parent directory |
| * @param projectName |
| * project name |
| * @return the project with a location pointing to the local file system |
| * @throws Exception |
| */ |
| public IProject createProjectInLocalFileSystem(File parentFile, |
| String projectName) throws Exception { |
| IProject project = ResourcesPlugin.getWorkspace().getRoot() |
| .getProject(projectName); |
| if (project.exists()) { |
| project.delete(true, null); |
| } |
| File testFile = new File(parentFile, projectName); |
| if (testFile.exists()) |
| FileUtils.delete(testFile, FileUtils.RECURSIVE | FileUtils.RETRY); |
| |
| IProjectDescription desc = ResourcesPlugin.getWorkspace() |
| .newProjectDescription(projectName); |
| desc.setLocation(new Path(new File(parentFile, projectName).getPath())); |
| project.create(desc, null); |
| project.open(null); |
| return project; |
| } |
| |
| /** |
| * verifies that repository contains exactly the given files. |
| * @param repository |
| * @param paths |
| * @throws Exception |
| */ |
| public void assertRepositoryContainsFiles(Repository repository, |
| String[] paths) throws Exception { |
| Set<String> expectedfiles = new HashSet<>(); |
| expectedfiles.addAll(Arrays.asList(paths)); |
| try (TreeWalk treeWalk = new TreeWalk(repository)) { |
| treeWalk.addTree(repository.resolve("HEAD^{tree}")); |
| treeWalk.setRecursive(true); |
| while (treeWalk.next()) { |
| String path = treeWalk.getPathString(); |
| if (!expectedfiles.contains(path)) |
| fail("Repository contains unexpected expected file " |
| + path); |
| expectedfiles.remove(path); |
| } |
| } |
| if (expectedfiles.size() > 0) { |
| StringBuilder message = new StringBuilder( |
| "Repository does not contain expected files: "); |
| for (String path : expectedfiles) { |
| message.append(path); |
| message.append(" "); |
| } |
| fail(message.toString()); |
| } |
| } |
| |
| /** |
| * verifies that repository contains exactly the given files with the given |
| * content. Usage example:<br> |
| * |
| * <code> |
| * assertRepositoryContainsFiles(repository, "foo/a.txt", "content of A", |
| * "foo/b.txt", "content of B") |
| * </code> |
| * @param repository |
| * @param args |
| * @throws Exception |
| */ |
| public void assertRepositoryContainsFilesWithContent(Repository repository, |
| String... args) throws Exception { |
| HashMap<String, String> expectedfiles = mkmap(args); |
| try (TreeWalk treeWalk = new TreeWalk(repository)) { |
| treeWalk.addTree(repository.resolve("HEAD^{tree}")); |
| treeWalk.setRecursive(true); |
| while (treeWalk.next()) { |
| String path = treeWalk.getPathString(); |
| assertTrue(expectedfiles.containsKey(path)); |
| ObjectId objectId = treeWalk.getObjectId(0); |
| byte[] expectedContent = expectedfiles.get(path) |
| .getBytes("UTF-8"); |
| byte[] repoContent = treeWalk.getObjectReader().open(objectId) |
| .getBytes(); |
| if (!Arrays.equals(repoContent, expectedContent)) { |
| fail("File " + path + " has repository content " |
| + new String(repoContent, "UTF-8") |
| + " instead of expected content " |
| + new String(expectedContent, "UTF-8")); |
| } |
| expectedfiles.remove(path); |
| } |
| } |
| if (expectedfiles.size() > 0) { |
| StringBuilder message = new StringBuilder( |
| "Repository does not contain expected files: "); |
| for (String path : expectedfiles.keySet()) { |
| message.append(path); |
| message.append(" "); |
| } |
| fail(message.toString()); |
| } |
| } |
| |
| /** |
| * Waits at least 100 milliseconds until no jobs of given family are running |
| * |
| * @param maxWaitTime |
| * @param family |
| * @throws InterruptedException |
| */ |
| public static void waitForJobs(long maxWaitTime, Object family) |
| throws InterruptedException { |
| waitForJobs(100, maxWaitTime, family); |
| } |
| |
| /** |
| * Waits at least <code>minWaitTime</code> milliseconds until no jobs of |
| * given family are running |
| * |
| * @param maxWaitTime |
| * @param minWaitTime |
| * @param family |
| * can be null which means all job families |
| * @throws InterruptedException |
| */ |
| public static void waitForJobs(long minWaitTime, long maxWaitTime, |
| Object family) |
| throws InterruptedException { |
| long start = System.currentTimeMillis(); |
| Thread.sleep(minWaitTime); |
| IJobManager jobManager = Job.getJobManager(); |
| Job[] jobs = jobManager.find(family); |
| while (busy(jobs)) { |
| Thread.sleep(50); |
| jobs = jobManager.find(family); |
| if (System.currentTimeMillis() - start > maxWaitTime) { |
| if (busy(jobs)) { |
| System.out.println("Following jobs were still running: " |
| + getJobNames(jobs)); |
| } |
| return; |
| } |
| } |
| } |
| |
| private static String getJobNames(Job[] jobs) { |
| StringBuilder sb = new StringBuilder(); |
| for (Job job : jobs) { |
| sb.append(job.getName()).append(" / ").append(job.toString()) |
| .append(", "); |
| } |
| return sb.toString(); |
| } |
| |
| private static boolean busy(Job[] jobs) { |
| for (Job job : jobs) { |
| int state = job.getState(); |
| if (state == Job.RUNNING || state == Job.WAITING) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static HashMap<String, String> mkmap(String... args) { |
| if ((args.length % 2) > 0) |
| throw new IllegalArgumentException("needs to be pairs"); |
| HashMap<String, String> map = new HashMap<>(); |
| for (int i = 0; i < args.length; i += 2) { |
| map.put(args[i], args[i+1]); |
| } |
| return map; |
| } |
| |
| public static String dumpThreads() { |
| final StringBuilder dump = new StringBuilder(); |
| final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); |
| final ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads( |
| threadMXBean.isObjectMonitorUsageSupported(), |
| threadMXBean.isSynchronizerUsageSupported()); |
| for (ThreadInfo threadInfo : threadInfos) { |
| dump.append("Thread ").append(threadInfo.getThreadId()).append(' ') |
| .append(threadInfo.getThreadName()).append(' ') |
| .append(threadInfo.getThreadState()).append('\n'); |
| LockInfo blocked = threadInfo.getLockInfo(); |
| if (blocked != null) { |
| dump.append(" Waiting for ").append(blocked); |
| String lockOwner = threadInfo.getLockOwnerName(); |
| if (lockOwner != null && !lockOwner.isEmpty()) { |
| dump.append(" held by ").append(lockOwner).append("(id=") |
| .append(threadInfo.getLockOwnerId()).append(')'); |
| } |
| dump.append('\n'); |
| } |
| for (LockInfo lock : threadInfo.getLockedSynchronizers()) { |
| dump.append(" Holding ").append(lock).append('\n'); |
| } |
| for (StackTraceElement s : threadInfo.getStackTrace()) { |
| dump.append(" at ").append(s).append('\n'); |
| } |
| } |
| return dump.toString(); |
| } |
| |
| /** |
| * Delete a project and repeat multiple times in case of resource deletion |
| * errors. A {@link ICoreRunnable} is used to avoid concurrent activities |
| * disturbing the deletion. |
| * |
| * @param project |
| * |
| * @throws CoreException |
| */ |
| public static void deleteProject(IProject project) throws CoreException { |
| ResourcesPlugin.getWorkspace().run(monitor -> { |
| // Following code inspired by {@link |
| // org.eclipse.jdt.testplugin.JavaProjectHelper#delete(IResource)}. |
| // Sometimes resource deletion may fail due to concurrently held |
| // locks. |
| for (int i = 0; i < MAX_DELETE_RETRY; i++) { |
| try { |
| project.delete( |
| IResource.FORCE |
| | IResource.ALWAYS_DELETE_PROJECT_CONTENT, |
| null); |
| break; |
| } catch (CoreException e) { |
| if (i == MAX_DELETE_RETRY - 1) { |
| throw e; |
| } |
| try { |
| org.eclipse.egit.core.Activator.logInfo( |
| "Sleep before retrying to delete project " |
| + project.getLocationURI()); |
| // Give other threads the time to close and release |
| // the resource. |
| Thread.sleep(DELETE_RETRY_DELAY); |
| } catch (InterruptedException e1) { |
| // Ignore and retry to delete |
| } |
| } |
| } |
| }, null); |
| } |
| |
| } |