blob: 41359633931fc8a744bfbab7e7b757eb78d5ab7a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.team.tests.ccvs.ui;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import junit.framework.Assert;
import org.eclipse.compare.structuremergeviewer.DiffNode;
import org.eclipse.compare.structuremergeviewer.IDiffContainer;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.resources.IContainer;
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.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.wizard.IWizard;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.team.internal.ccvs.core.CVSStatus;
import org.eclipse.ui.internal.dialogs.InternalErrorDialog;
import org.eclipse.ui.wizards.datatransfer.ImportOperation;
import org.eclipse.ui.wizards.datatransfer.ZipFileStructureProvider;
/**
* Provides helpers for:
* <ul>
* <li>Resource manipulation</li>
* <li>Diff trees</li>
* <li>UI automation</li>
* <li>Parallel development simulation</li>
* </ul>
*
* Note: This class is referenced from the VCM 1.0 performance tests.
*/
public class Util {
/*** RESOURCE MANIPULATION SUPPORT ***/
/**
* Gets a handle for a project of a given name.
* @param name the project name
* @return the project handle
*/
public static IProject getProject(String name) throws CoreException {
return ResourcesPlugin.getWorkspace().getRoot().getProject(name);
}
/**
* Creates a new project.
* @param name the project name
* @return the project handle
*/
public static IProject createProject(String name) throws CoreException {
IProject project = getProject(name);
if (!project.exists()) project.create(null);
if (!project.isOpen()) project.open(null);
return project;
}
/**
* Deletes a project.
* @param project the project
*/
public static void deleteProject(IProject project) throws CoreException {
project.delete(false /*force*/, null);
}
/**
* Deletes a file and prunes empty containing folders.
* @param file the file to delete
*/
public static void deleteFileAndPrune(IFile file) throws CoreException {
file.delete(false /*force*/, null);
IContainer container = file.getParent();
while (container != null && container instanceof IFolder &&
isFolderEmpty((IFolder) container)) {
deleteFolder((IFolder) container);
container = container.getParent();
}
}
/**
* Deletes a folder.
*/
public static void deleteFolder(IFolder folder) throws CoreException {
try {
folder.delete(false /*force*/, null);
} catch (CoreException e) {
IStatus status = e.getStatus();
// ignore errors caused by attempting to delete folders that CVS needs to have around
if (findStatusByCode(status, CVSStatus.FOLDER_NEEDED_FOR_FILE_DELETIONS) == null) {
throw e;
}
}
}
/**
* Finds an IStatus instance in a multi-status by status code.
*/
public static IStatus findStatusByCode(IStatus status, int code) {
if (status.getCode() == code) return status;
IStatus[] children = status.getChildren();
for (int i = 0; i < children.length; i++) {
IStatus found = findStatusByCode(children[i], code);
if (found != null) return found;
}
return null;
}
/**
* Creates a uniquely named project.
* @param prefix a string prepended to the generated name
* @return the new project
*/
public static IProject createUniqueProject(String prefix) throws CoreException {
return createProject(makeUniqueName(null, prefix, null));
}
/**
* Creates a uniquely named file in the parent folder or project with random contents.
* @param gen the sequence generator
* @param parent the parent IFolder or IProject for the new file
* @param meanSize the mean size of file to create (in bytes)
* @param variance 69% of files with be within this amount of the mean
* @param probBinary the probability of a new file being binary as a percentage
* @return the new file
*/
public static IFile createUniqueFile(SequenceGenerator gen, IContainer parent,
int meanSize, int variance, int probBinary) throws IOException, CoreException {
int fileSize;
do {
fileSize = (int) Math.abs(gen.nextGaussian() * variance + meanSize);
} while (fileSize > meanSize + variance * 4); // avoid huge files
ByteArrayOutputStream os = new ByteArrayOutputStream();
String fileName;
if (gen.nextInt(100) < probBinary) {
fileName = makeUniqueName(gen, "file", "class"); // binary
writeRandomBytes(gen, os, fileSize);
} else {
fileName = makeUniqueName(gen, "file", "txt"); // text
writeRandomText(gen, os, fileSize);
}
IFile file = parent.getFile(new Path(fileName));
file.create(new ByteArrayInputStream(os.toByteArray()), true, null);
os.close();
return file;
}
/**
* Creates a uniquely named folder in the parent folder.
* @param gen the sequence generator
* @param parent the parent IFolder or IProject for the new folder
* @return the new folder
*/
public static IFolder createUniqueFolder(SequenceGenerator gen, IContainer parent) throws CoreException {
IFolder folder = parent.getFolder(new Path(Util.makeUniqueName(gen, "folder", null)));
folder.create(false /*force*/, true /*local*/, null);
return folder;
}
/**
* Renames a resource.
* The resource handle becomes invalid.
* @param resource the existing resource
* @param newName the new name for the resource
*/
public static void renameResource(IResource resource, String newName) throws CoreException {
switch (resource.getType()) {
case IResource.PROJECT: {
IProject project = (IProject) resource;
IProjectDescription desc = project.getDescription();
desc.setName(newName);
project.move(desc, false /*force*/, true /*keepHistory*/, null);
} break;
case IResource.FOLDER: {
try {
resource.move(new Path(newName), false /*force*/, null);
} catch (CoreException e) {
IStatus status = e.getStatus();
// ignore errors caused by attempting to delete folders that CVS needs to have around
if (findStatusByCode(status, CVSStatus.FOLDER_NEEDED_FOR_FILE_DELETIONS) == null) {
throw e;
}
}
} break;
default:
resource.move(new Path(newName), false /*force*/, null);
break;
}
}
/**
* Modified a resource.
* @param gen the sequence generator
* @param file the file to modify
*/
public static void modifyFile(SequenceGenerator gen, IFile file)
throws IOException, CoreException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
InputStream is = file.getContents(true);
try {
byte[] buffer = new byte[8192];
int rsize;
boolean changed = false;
while ((rsize = is.read(buffer)) != -1) {
double gaussian;
do {
gaussian = gen.nextGaussian() * 0.5; // large changes are less likely than small ones
} while (gaussian > 1.0 || gaussian < -1.0);
int changeSize = (int) (gaussian * rsize);
changed = changed || changeSize != 0;
os.write(buffer, 0, changeSize < 0 ? - changeSize : rsize); // shrink file
writeRandomText(gen, os, changeSize); // enlarge file
}
if (! changed) os.write('!'); // make sure we actually did change the file
file.setContents(new ByteArrayInputStream(os.toByteArray()), false /*force*/, true /*keepHistory*/, null);
} finally {
is.close();
}
} finally {
os.close();
}
}
/**
* Creates a unique name.
* Ensures that a deterministic sequence of names is generated for all files
* and folders within a project, though not across sessions.
*
* @param gen the generator, or null if this name is to be globally unique
* @param prefix a string prepended to the generated name
* @param extension the file extension not including the period, null if none
* @return the new name
*/
public static String makeUniqueName(SequenceGenerator gen, String prefix, String extension)
throws CoreException {
StringBuffer name = new StringBuffer(prefix);
name.append('-');
if (gen == null) {
name.append(SequenceGenerator.nextGloballyUniqueLong());
} else {
name.append(gen.nextUniqueInt());
}
if (extension != null) {
name.append('.');
name.append(extension);
}
return name.toString();
}
/**
* Imports a .zip file into a container's root folder.
* @param container the container
* @param file the path of the .zip file
*/
public static void importZip(IContainer container, File file)
throws IOException, ZipException, InterruptedException, InvocationTargetException {
ZipFile zipFile = new ZipFile(file);
ZipFileStructureProvider provider = new ZipFileStructureProvider(zipFile);
ImportOperation importOperation = new ImportOperation(container.getFullPath(),
provider.getRoot(), provider, null);
importOperation.setOverwriteResources(true); // don't ask
importOperation.run(new NullProgressMonitor());
Assert.assertTrue(importOperation.getStatus().isOK());
}
/**
* Writes random text to an output stream.
* @param gen the sequence generator
*/
public static void writeRandomText(SequenceGenerator gen, OutputStream os, int count) throws IOException {
while (count-- > 0) {
int c = gen.nextInt(99);
os.write((c >= 95) ? '\n' : c + ' ');
}
}
/**
* Writes random bytes to an output stream.
* @param gen the sequence generator
*/
public static void writeRandomBytes(SequenceGenerator gen, OutputStream os, int count) throws IOException {
while (count-- > 0) {
os.write(gen.nextInt(256));
}
}
/**
* Creates a random folder deeply below the root folder.
* @param gen the sequence generator
* @param root the root IFolder or IProject for the operation
* @return the new folder
*/
public static IFolder createRandomDeepFolder(SequenceGenerator gen, IContainer root) throws CoreException {
IContainer container = pickRandomDeepContainer(gen, root);
for (;;) {
IFolder folder = createUniqueFolder(gen, container);
container = folder;
// 12.5% chance of creating a nested folder
if (gen.nextInt(8) != 0) return folder;
}
}
/**
* Creates several random files deeply below the root folder.
* @param gen the sequence generator
* @param root the root IFolder or IProject for the operation
* @param count the number of files to create
* @param meanSize the mean size of file to create (in bytes)
* @param probBinary the probability of a new file being binary as a percentage
*/
public static void createRandomDeepFiles(SequenceGenerator gen, IContainer root, int count,
int meanSize, int variance, int probBinary) throws IOException, CoreException {
while (count-- > 0) {
createUniqueFile(gen, pickRandomDeepContainer(gen, root), meanSize, variance, probBinary);
}
}
/**
* Deletes several random files deeply below the root folder.
* @param gen the sequence generator
* @param root the root IFolder or IProject for the operation
* @param count the number of files to delete
*/
public static void deleteRandomDeepFiles(SequenceGenerator gen, IContainer root, int count) throws CoreException {
while (count-- > 0) {
IFile file = pickRandomDeepFile(gen, root);
if (file == null) break;
deleteFileAndPrune(file);
}
}
/**
* Modifies several random files deeply below the root folder.
* @param gen the sequence generator
* @param root the root IFolder or IProject for the operation
* @param count the number of files to modify
*/
public static void modifyRandomDeepFiles(SequenceGenerator gen, IContainer root, int count)
throws IOException, CoreException {
// perhaps we can add a parameter for the "magnitude" of the change
while (count-- > 0) {
IFile file = pickRandomDeepFile(gen, root);
if (file == null) break;
modifyFile(gen, file);
}
}
/**
* Touches several random files deeply below the root folder.
* @param gen the sequence generator
* @param root the root IFolder or IProject for the operation
* @param count the number of files to touch
*/
public static void touchRandomDeepFiles(SequenceGenerator gen, IContainer root, int count) throws CoreException {
while (count-- > 0) {
IFile file = pickRandomDeepFile(gen, root);
if (file == null) break;
file.touch(null);
}
}
/**
* Renames several random files deeply below the root folder.
* @param gen the sequence generator
* @param root the root IFolder or IProject for the operation
* @param count the number of files to touch
*/
public static void renameRandomDeepFiles(SequenceGenerator gen, IContainer root, int count) throws CoreException {
IProject project = root.getProject();
while (count-- > 0) {
IFile file = pickRandomDeepFile(gen, root);
if (file == null) break;
renameResource(file, makeUniqueName(gen, "file", file.getFileExtension()));
}
}
/**
* Picks a random file from the parent folder or project.
* @param gen the sequence generator
* @param parent the parent IFolder or IProject for the operation
* @return the file that was chosen, or null if no suitable files
*/
public static IFile pickRandomFile(SequenceGenerator gen, IContainer parent) throws CoreException {
IResource[] members = filterResources(parent.members());
for (int size = members.length; size != 0; --size) {
int elem = gen.nextInt(size);
if (members[elem] instanceof IFile) return (IFile) members[elem];
System.arraycopy(members, elem + 1, members, elem, size - elem - 1);
}
return null;
}
/**
* Picks a random folder from the parent folder or project.
* @param gen the sequence generator
* @param parent the parent IFolder or IProject for the operation
* @return the folder, or null if no suitable folders
*/
public static IFolder pickRandomFolder(SequenceGenerator gen, IContainer parent) throws CoreException {
IResource[] members = filterResources(parent.members());
for (int size = members.length; size != 0; --size) {
int elem = gen.nextInt(size);
if (members[elem] instanceof IFolder) return (IFolder) members[elem];
System.arraycopy(members, elem + 1, members, elem, size - elem - 1);
}
return null;
}
/**
* Picks a random file deeply from the root folder or project.
* @param gen the sequence generator
* @param root the root IFolder or IProject for the operation
* @return the file that was chosen, or null if no suitable files
*/
public static IFile pickRandomDeepFile(SequenceGenerator gen, IContainer root) throws CoreException {
IResource[] members = filterResources(root.members());
for (int size = members.length; size != 0; --size) {
int elem = gen.nextInt(size);
IResource resource = members[elem];
if (resource instanceof IFile) return (IFile) resource;
if (resource instanceof IFolder) {
IFile file = pickRandomDeepFile(gen, (IFolder) resource);
if (file != null) return file;
}
System.arraycopy(members, elem + 1, members, elem, size - elem - 1);
}
return null;
}
/**
* Picks a random folder deeply from the root folder or project.
* May pick the project's root container.
* @param gen the sequence generator
* @param root the root IFolder or IProject for the operation
* @return the container that was chosen, never null
*/
public static IContainer pickRandomDeepContainer(SequenceGenerator gen, IContainer root) throws CoreException {
if (gen.nextInt(6) == 0) {
IResource[] members = filterResources(root.members());
for (int size = members.length; size != 0; --size) {
int elem = gen.nextInt(size);
IResource resource = members[elem];
if (resource instanceof IFolder) {
return pickRandomDeepContainer(gen, (IFolder) resource);
}
System.arraycopy(members, elem + 1, members, elem, size - elem - 1);
}
}
Assert.assertTrue(isValidContainer(root));
return root;
}
/**
* Returns true if the folder does not contain any real files.
*/
public static boolean isFolderEmpty(IFolder folder) throws CoreException {
IResource[] members = folder.members();
for (int i = 0; i < members.length; ++i) {
if (isValidFile(members[i]) || isValidFolder(members[i])) return false;
}
return true;
}
/**
* Returns true iff file is a valid IFile (that should not be ignored).
*/
public static boolean isValidFile(IResource file) throws CoreException {
String name = file.getName();
return file instanceof IFile
&& ! file.isPhantom()
&& ! name.equals(".classpath")
&& ! name.equals(".project")
&& ! name.equals(".vcm_meta");
}
/**
* Returns true iff folder is a valid IFolder (that should not be ignored).
*/
public static boolean isValidFolder(IResource folder) throws CoreException {
String name = folder.getName();
return folder instanceof IFolder
&& ! folder.isPhantom()
&& ! name.equals("CVS")
&& ! name.equals("bin");
}
/**
* Returns true iff container is a valid IFolder or IProject (that should not be ignored).
*/
public static boolean isValidContainer(IResource container) throws CoreException {
return container instanceof IProject || isValidFolder(container);
}
/**
* Returns true iff resource is a valid IFile, IFolder or IProject (that should not be ignored).
*/
public static boolean isValidResource(IResource resource) throws CoreException {
return isValidFile(resource) || isValidContainer(resource);
}
/**
* Filters and sorts an array of resources to ensure deterministic behaviour across
* sessions. The general idea is to guarantee that given a known sequence of
* pseudo-random numbers, we will always pick the same sequence of files and
* folders each time we repeat the test.
*/
public static IResource[] filterResources(IResource[] resources) throws CoreException {
List list = new ArrayList(resources.length);
for (int i = 0; i < resources.length; ++i) {
if (isValidResource(resources[i])) list.add(resources[i]);
}
if (list.size() != resources.length) {
resources = (IResource[]) list.toArray(new IResource[list.size()]);
}
Arrays.sort(resources, new Comparator() {
public int compare(Object a, Object b) {
return ((IResource) a).getName().compareTo(((IResource) b).getName());
}
});
return resources;
}
/*** DIFF SUPPORT ***/
public static boolean isEmpty(IDiffContainer node) {
if (node == null) return true;
if (node.getKind() != 0) return false;
IDiffElement[] children = node.getChildren();
for (int i = 0; i < children.length; i++) {
if (!isEmpty(children[i])) return false;
}
return true;
}
public static boolean isEmpty(IDiffElement element) {
if (element == null) return true;
if (element.getKind() != 0) return false;
if (element instanceof IDiffContainer) {
IDiffElement[] children = ((DiffNode)element).getChildren();
for (int i = 0; i < children.length; i++) {
if (!isEmpty(children[i])) return false;
}
}
return true;
}
/*** UI SUPPORT ***/
/**
* Opens the specified wizard, then notifies the waiter.
* The WizardDialog instance is passed as argument to notify() in the waiter.
*/
public static void waitForWizardToOpen(Shell parent, IWizard wizard, final Waiter waiter) {
WizardDialog dialog = new WizardDialog(parent, wizard) {
public int open() {
// create the window's controls
create();
// hook into the event loop so we get called back when the wizard is up and running
final Display display = getContents().getDisplay();
final WizardDialog dialog = this;
display.asyncExec(new Runnable() {
public void run() {
while (display.readAndDispatch()); // process any other pending messages first
waiter.notify(dialog);
}
});
// call open (does not create the window's controls a second time)
return super.open();
}
};
dialog.open();
}
/**
* Notifies the waiter when a Shell matching the specified criteria opens.
* The Shell instance is passed as argument to notify() in the waiter.
*
* @param display the root display
* @param pollingPeriod the number of milliseconds to wait between polls
* @param value a value used for matching
* @param criteria a strategy for matching the controls with a value,
* or null to match any Shell.
* @param waiter the waiter to be notified
*/
public static void waitForShellToOpen(final Display display, final int pollingPeriod,
final Object value, final ICriteria criteria, final Waiter waiter) {
final Runnable hook = new Runnable() {
public void run() {
if (display.isDisposed()) return;
Shell[] shells = display.getShells();
for (int i = 0; i < shells.length; ++i) {
Shell shell = shells[i];
if (criteria != null && ! criteria.test(shell, value)) continue;
if (! waiter.notify(shell)) return;
}
// poll again as soon as possible
if (waiter.keepWaiting()) {
display.timerExec(pollingPeriod, this);
}
}
};
hook.run();
}
/**
* Installs a watchdog for JFace error dialogs for the current display.
* The Dialog instance is passed as argument to notify() in the waiter.
* Recognized dialogs:
* - ErrorDialog
* - InternalErrorDialog
*
* @param display the root display
* @param pollingPeriod the number of milliseconds to wait between polls
* @param waiter the waiter to be notified
*/
public static void waitForErrorDialog(Display display, int pollingPeriod, final Waiter waiter) {
ICriteria criteria = new ICriteria() {
public boolean test(Object candidate, Object value) {
Shell shell = (Shell) candidate;
if (shell.isDisposed()) return false;
Object data = shell.getData();
if (data == null) return false;
return data instanceof ErrorDialog || data instanceof InternalErrorDialog;
}
};
waitForShellToOpen(display, pollingPeriod, null, criteria, new Waiter() {
public boolean keepWaiting() {
return waiter.keepWaiting();
}
public boolean notify(Object object) {
return waiter.notify(((Shell) object).getData());
}
});
}
/**
* Finds a Control in a Composite hierarchy matching the specified criteria.
*
* @param root the root of the hierarchy to search
* @param clazz the Class representing the precise type of Control to find
* @param value a value used for matching
* @param criteria a strategy for matching the controls with a value,
* or null to match anything of the right class.
* @return the first matching Control, or null if none found.
*/
public static Control findControl(Composite root, Class clazz, Object value, ICriteria criteria) {
if (clazz.isAssignableFrom(root.getClass())) {
if (criteria == null || criteria.test(root, value)) return root;
}
Control[] children = root.getChildren();
for (int i = 0; i < children.length; ++i) {
final Control candidate = children[i];
if (candidate instanceof Composite) {
Control c = findControl((Composite) candidate, clazz, value, criteria);
if (c != null) return c;
} else {
if (clazz.isAssignableFrom(candidate.getClass())) {
if (criteria == null || criteria.test(candidate, value)) return candidate;
}
}
}
return null;
}
/**
* Finds a Control in a Composite hierarchy with the specified text string.
* Note: clazz must specify a Control subclass that defines getText()
*
* @param root the root of the hierarchy to search
* @param clazz the Class representing the precise type of Control to find
* @param text the text string to find
* @return the first matching Control, or null if none found.
*/
public static Control findControlWithText(Composite root, Class clazz, String text) {
return findControl(root, clazz, text, new ICriteria() {
public boolean test(Object control, Object value) {
// getText is only defined on certain subclasses of Composite
// so we must use reflection to find the method
try {
Method m = control.getClass().getMethod("getText", new Class[0]);
String text = (String) m.invoke(control, new Object[0]);
return value.equals(stripMnemonicEscapes(text));
} catch (Exception e) {
e.printStackTrace();
Assert.fail("Could not invoke method getText()");
}
return false;
}
});
}
/**
* Posts a fake event to the queue.
* Fills in the event type and widget fields.
* @param event the Event
*/
public static void postEvent(final Widget widget, final int eventType, final Event event) {
Display display = widget.getDisplay();
event.type = eventType;
event.widget = widget;
display.asyncExec(new Runnable() {
public void run() {
widget.notifyListeners(eventType, event);
}
});
}
/**
* Strips mnemonic escapes from a text label.
*/
public static String stripMnemonicEscapes(String label) {
StringBuffer buf = new StringBuffer();
int length = label.length();
for (int i = 0; i < length; ++i) {
char c = label.charAt(i);
if (c == '&') {
i += 1;
if (i < length) c = label.charAt(i);
}
buf.append(c);
}
return buf.toString();
}
/**
* Process pending events for the current display, until at least the
* specified number of milliseconds elapses.
*/
public static void processEventsUntil(int hiatus) {
Display display = Display.getCurrent();
Assert.assertNotNull(display);
final boolean done[] = new boolean[] { hiatus == 0 };
if (hiatus != 0) display.timerExec(hiatus, new Runnable() {
public void run() { done[0] = true; }
});
for (;;) {
while (display.readAndDispatch());
if (done[0]) return;
display.sleep();
}
}
/**
* Process pending events for the current display, until resumed by the user.
* Very useful for inspecting intermediate results while debugging.
*/
public static void processEventsUntilResumed(String title) {
Display display = Display.getCurrent();
Assert.assertNotNull(display);
Shell shell = new Shell(display, SWT.CLOSE);
shell.setText("Close me to resume: " + title);
shell.setBounds(0, 0, 500, 30);
shell.open();
while (! shell.isDisposed()) {
while (! display.readAndDispatch()) display.sleep();
}
}
}