Tuesday, September 23, 2008

Eclipse GUI Testing Is Viable With SWTBot

For several years I've struggled with the viability of automated Eclipse GUI test frameworks. Now for the first time I've found an approach that works reliably. This article discusses the approach and details some specific techniques that greatly increase productivity in creating useful tests.

In the past automated GUI test frameworks have failed to meet my needs because they have focused too much on recording and playback. The complexity of the Eclipse environment makes it near impossible to get this approach to work reliably. Simple things such as background jobs and processing become major issues. Testing of GEF-based editors is not possible since all record/playback frameworks rely on identifying controls and widgets -- which GEF does not use.

Recently I've come across SWTBot, which uses a novel approach. SWTBot tests are written in Java, and run inside the Eclipse process as an Eclipse test application. This gives SWTBot full access to the SWT and Eclipse APIs. SWTBot tests are written as JUnits, which makes integration with common technologies such as Ant, Continuous Integration and code coverage tools easy.

But all is not rosy when writing tests with SWTBot. There are some Eclipse-specific idiosyncrasies that come back to bite you such as context menus that are reconstructed when shown and the GEF framework that does not use controls or widgets. That said, SWTBot provides and excellent starting point. Here's how I built on SWTBot to create a powerful test environment that is easy to use.

Initially my tests contained code that looks like this:


public void resetPerspective() {
bot.menu("Window").menu("Reset Perspective...").click();
bot.shell("Reset Perspective").activate();
bot.button("OK").click();
}

Not bad! It's easy to see what's going on. In other cases the code looked more like this:


SWTBotTree tree = bot.tree();
String[] path = name.split("/");
SWTBotTreeItem[] items = tree.getAllItems();
SWTBotTreeItem selectedItem = null;
for (SWTBotTreeItem item: items) {
if (path[0].equals(item.getText())) {
item.expand();
sleep();
selectedItem = item;
break;
}
}
for (int x = 1;selectedItem != null && x

As you can see, it's hard to see the forest for the trees. Way too much code is required to do simple things.

Here's what I did to make things easier:

1. Create classes that directly model the UI parts that you're working with in your tests. For example, instead of directly using SWTBotView to manipulate the Package Explorer view, create a class called PackageExplorer that delegates to SWTBotView and provides richer functionality. For example, in my PackageExplorer class I have the following method:


/**
* select the first element that adapts to the given resource
*/
public void select(final IResource resource)

2. Create a method to find and click context menu items in one go. In SWTBot the finding and clicking occur in two seperate UI runnables. In Eclipse this can cause problems for some context menus as the menu item gets disposed before it is clicked due to a loss of focus.

3. Instead of using sleep() to wait for something to be done, wait for the real thing to be done. A heavily loaded machine can cause processing times to vary. Instead of having a fragile sleep(500L), use a reliable technique to determine when the job is really done.
For example, if you know that your processing is holding a resource lock, post a no-op empty workspace job and wait on it inside your test. It will only be invoked once all other resource locks are released, so when it's complete you're guaranteed that your other job is done:


// ensure that all queued workspace operations and locks are released
ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
public void run(IProgressMonitor monitor) throws CoreException {
// nothing to do!
}
}, new NullProgressMonitor());

4. Leverage and extend the SWTBot framework with Eclipse-specific behaviour. For example, make use of SWTBot's conditional waiting APIs by creating Eclipse-specific conditions like this one that is used to wait until an editor is opened on a resource:


/**
* a condition that is used to wait for an editor to open on a specific file.
*
* @author dgreen
*/
public class EditorOpenCondition extends DefaultCondition {
private final IFile file;

public EditorOpenCondition(IFile file) {
this.file = file;
}

public String getFailureMessage() {
return String.format("Timed out waiting for editor on %s to open",file.getFullPath());
}

public boolean test() throws Exception {
if (!file.exists()) {
return false;
}
return UIThreadRunnable.syncExec(new UIThreadRunnable.BoolResult() {
public boolean run() {
IEditorReference[] editorReferences = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getEditorReferences();
for (IEditorReference reference: editorReferences) {
try {
IEditorInput input = reference.getEditorInput();
if (input instanceof IFileEditorInput) {
IFileEditorInput editorInput = (IFileEditorInput) input;
if (editorInput.getFile().equals(file)) {
return true;
}
}
if (input instanceof IStorageEditorInput) {
IStorageEditorInput editorInput = (IStorageEditorInput) input;
IPath fullPath = editorInput.getStorage().getFullPath();
if (fullPath.equals(file.getFullPath())) {
return true;
}
}
} catch (PartInitException e) {
e.printStackTrace();
} catch (CoreException e) {
e.printStackTrace();
}
}
return false;
}
});
}
}

5. Create a Java-based DSL for often-repeated use of editors or views. For example, for a static class diagram editor you might end up with a DSL that could be used as follows:


DomainModelDsl domainModelDsl = new DomainModelDsl();
domainModelDsl.create(project, "domain.dm");

domainModelDsl.
createEntity("A").
createEntity("B");

domainModelDsl.entity("A").extension("B");

Inside the DSL implementation the dirty work of manipulating the editor occurs. This makes it really fast to create complex tests that cover lots of ground.

6. Create GEF EditPart wappers. For GEF-based editors SWTBot doesn't give a lot of lift. For these you'll need to create classes similar to those provided by SWTBot, but instead of being widget-focused, they'll need to be EditPart-focused.

All in all I'm very impressed with SWTBot, which has finally delivered a viable automated GUI test framework for Eclipse and Eclipse RCP applications. I'm pleased to see that SWTBot has made an Eclipse project proposal, which if approved will hopefully lead to continued improvements and community adoption.

Monday, September 8, 2008

Beyond Rich Text (Part 2): Displaying Images in an SWT StyledText

Recently I wrote about a few simple tricks that make it possible to display more than just text in a StyledText widget. In this article I take it a step further: displaying images in an SWT StyledText widget.

Normally a StyledText widget cannot display images. Why would you want to display images in a text control, you might ask? Well, there are cases where a rich text control is used to render text which may have embedded images. Take for example the Mylyn task editor. It displays bug descriptions and comments. In some repositories (such as Trac or JIRA) it's possible to use markup to display rich text and images. It's common to see screenshots or other visual data included in bug reports.

To display images in a StyledText we use a small variation on the tricks from my previous post. Previously I used an annotation to mark the location of the thing I wanted to draw, and a painter to paint it. This approach works well when we know the size of the thing we're about to draw (such as a character or horizontal rule). In the case of images, it gets more complicated. We need to know the size of the image so that we can create enough space for it in the text.

Since loading image data can be slow (over a network or from a busy disk) we must load the image asynchronously and render it when it becomes available. We can get away with it since users are used to this experience -- that's how web browsers work.

So here's a run-down of the technique I used:
  1. Mark the location of the image with an annotation
  2. Asynchronously download the image
  3. When the image becomes available, determine its height (in pixels)
  4. Determine the font in use at the annotated location
  5. Using font metrics compute the number of blank lines required to create enough vertical space (in pixels) to display the image without obscuring text
  6. Insert the required number of newlines into the text widget
  7. Adjust text presentation offsets (some styled regions may need to be translated by the same number of characters as we inserted in the previous step)
  8. Associate the image data with the annotation so that the painter will have access to it
  9. Cause the text widget to repaint
  10. Use the custom painter to paint the image in the empty space that we created
Here's a screenshot demonstrating this technique in action:
It works! The SWT StyledText can now display images!

Though the result is pleasing, there remain a few issues with this approach:
  • Images don't flow with the text, that is we can't wrap text around the image on the left or right side, only above and below.
  • Images aren't included in clipboard operations such as copy and paste.
If you're interested in the code involved in this technique you can find it here.

The fact that we can do this using standard JFace and SWT APIs is a testament to the quality and completeness of the platform. Thanks Eclipse!

Tuesday, September 2, 2008

Beyond Rich Text: Tricks Using SourceViewer, Annotations and AnnotationPainter


An SWT StyledText can display text attributes such as bold, italic and strikethrough, alter colors and fonts. What are we to do when we need to go beyond rich text formatting? In this article we present three simple tricks using standard SWT and JFace APIs to create a polished presentation where text attributes alone won't do the job.

Marking The Spot


In order to display something interesting, we need to mark the spot. Normally this is done with a TextPresentation, which specifies character offsets and style ranges. Fortunately the Eclipse APIs give us another mechanism to mark regions of text in an extensible manner: annotations.

The key APIs at our disposal are as follows:

org.eclipse.jface.text.source.Annotation
org.eclipse.jface.text.source.AnnotationModel

Using these APIs we can create annotations and specify their location. An easy place to do this is in your document partitioner. It will be called at the appropriate times to partition your document. By using a RuleBasedPartitionScanner you can modify your rules to create the appropriate annotations.

Now that our document is annotated, we know where we need to draw. To hook up the drawing strategy, we add the following code to the initialization of the SourceViewer:


IAnnotationAccess annotationAccess = new IAnnotationAccess() {
public Object getType(Annotation annotation) {
return annotation.getType();
}
public boolean isMultiLine(Annotation annotation) {
return true;
}
public boolean isTemporary(Annotation annotation) {
return true;
}
};

AnnotationPainter painter = new AnnotationPainter(sourceViewer, annotationAccess);


Now all we have to do is add a drawing strategy to the painter for every kind of annotation that we're interested in drawing. Read on to find out how we do that.

Repainting Characters


Not all fonts can display all characters. This is problematic in an application that is internationalized or one where the user can change the font.

Take for example bullets. Unicode \u2022 can be used to display a solid round bullet with most fonts, but what about an empty one, or a square one? These characters cannot be reliably found in commonly used fonts. The trick we use is to always use the \u2022 character, but repaint it where we want to display something more interesting. By doing that we get the display just right and the text works nicely with copy/paste operations.

To make it work, we create annotations where our bullet characters are in the document. We then hook up a bullet drawing strategy to our painter as follows:


painter.addDrawingStrategy(BulletAnnotation.TYPE, new BulletDrawingStrategy());
painter.addAnnotationType(BulletAnnotation.TYPE, BulletAnnotation.TYPE);
painter.setAnnotationTypeColor(BulletAnnotation.TYPE, getTextWidget().getForeground());

The painter won't invoke our drawing strategy unless the type and type color are also added.

What does our bullet annotation look like? It needs to have enough information for the drawing strategy to know what to draw. In this case the shape of the bullet is dependent on the 'level' of indentation. Here's what I used:



public class BulletAnnotation extends Annotation {

public static final String TYPE = "org.eclipse.mylyn.internal.wikitext.ui.viewer.annotation.bullet";

private final int indentLevel;

public BulletAnnotation(int indentLevel) {
super(TYPE, false, Integer.toString(indentLevel));
this.indentLevel = indentLevel;
}

public int getIndentLevel() {
return indentLevel;
}

}


Now we need to implement our drawing strategy. The drawing strategy must 'erase' the existing bullet character and then draw the new bullet shape where the old bullet was.

We erase the previous character by drawing a rectangle the size of the character in the background color:


// erase whatever character was there
gc.fillRectangle(left.x, left.y, right.x - left.x, lineHeight);


then we draw the new shape:


// now paint the bullet
switch (bullet.getIndentLevel()) {
case 1: // round solid bullet
gc.setBackground(color);
gc.fillOval(hcenter - 3, vcenter - 2, 5, 5);
break;
case 2: // round empty bullet
gc.setForeground(color);
gc.drawOval(hcenter - 3, vcenter - 3, 5, 5);
break;
default: // square bullet
gc.setBackground(color);
gc.fillRectangle(hcenter - 3, vcenter - 2, 5, 5);
break;
}


Here's a screenshot showing an example of this technique in use:


Drawing Non-Characters


Sometimes there's a need to display non-characters. For example, browsers display a horizontal line for <hr /> (horizontal rule). By marking the spot with annotations and registering a custom painter, we can do the same thing. Here's the result we're looking for.



To create this effect we put an empty line in the text we're displaying and annotate it with a HorizontalRuleAnnotation. Drawing the annotation is easy:



public void draw(Annotation annotation, GC gc, StyledText textWidget, int offset, int length, Color color) {
if (gc != null) {
final Color foreground = gc.getForeground();

Point left = textWidget.getLocationAtOffset(offset);
Point right = textWidget.getLocationAtOffset(offset + length);
if (left.x > right.x) {
// hack: sometimes linewrapping text widget gives us the wrong x/y for the first character of a line that
// has been wrapped.
left.x = 0;
left.y = right.y;
}
right.x = textWidget.getClientArea().width;

int baseline = textWidget.getBaseline(offset);

int vcenter = left.y + (baseline / 2) + (baseline / 4);

gc.setLineWidth(0); // NOTE: 0 means width is 1 but with optimized performance
gc.setLineStyle(SWT.LINE_SOLID);

left.x += 3;
right.x -= 5;
vcenter -= 2;

if (right.x > left.x) {
// draw the shadow
gc.setForeground(shadowForeground);
gc.drawRectangle(left.x, vcenter, right.x - left.x, 2);

// draw the horizontal rule
gc.setForeground(color);
gc.drawLine(left.x, vcenter, right.x, vcenter);
gc.drawLine(left.x, vcenter, left.x, vcenter + 2);
}

gc.setForeground(foreground);
} else {
textWidget.redrawRange(offset, length, true);
}
}


As before, we hook the drawing strategy up to the painter:


painter.addDrawingStrategy(HorizontalRuleAnnotation.TYPE, new HorizontalRuleDrawingStrategy());
painter.addAnnotationType(HorizontalRuleAnnotation.TYPE, HorizontalRuleAnnotation.TYPE);
painter.setAnnotationTypeColor(HorizontalRuleAnnotation.TYPE, getTextWidget().getForeground());

Conclusion



Eclipse provides some powerful APIs for hooking into the painting of StyledText. Using some simple tricks we can create powerful polished visuals in an SWT user interface. All of these techniques are applied in the Mylyn WikiText project, where you can find source code that works.