Tuesday, July 11, 2006

Bugs in the Library

One of the great benefits of Java is the availability of a wide variety of libraries providing all sorts of functionality. However, managing bugs, workarounds, and fixes becomes more complicated when tracking multiple third-party libraries.

With a simple source code base or a single application, it's relatively simple to take library updates to get bug fixes, or even to apply patches to a local copy of a library. There are many situations where this isn't possible and you need to determine for a given section of client code whether a bug is present. One codebase may need to support several VM releases and what's a workaround in one may introduce a bug in another.

I keep track of library-based bugs with a class Bugs which comprises static methods which check the environment for the presence of a given bug. For instance, Mac OSX between Java releases 1.4 and 1.5 incorrectly mapped mouse button events generated by java.awt.Robot. So Bugs would have a corresponding method,


// This bug has been submitted to Apple, bug #XXXXXXX
// It was officially fixed in version 1.5
public static boolean hasRobotButtonSwapBug() {
return Platform.isMac() && Platform.JAVA_VERSION >= 0x1400
&& Platform.JAVA_VERSION < 0x1500;
}


The method contains checks which describe as precisely as possible the environment where the bug is present. In this case, several java releases were tested; the bug first appeared in the Java 1.4 release and persisted until 1.5. The method should narrowly identify only those environments shown to produce the bug (see below).

In the corresponding BugsTest, we implement a method which actually checks for the bug. This serves several purposes. First, it provides a test case to submit along with a bug report to whoever is responsible for the bug. Second, it provides a standard check when used with other automated tests to flag changes in released libraries. Checking the "bug presence" method at the start of the test avoids test failures where we already know the bug exists, and tests for its presence in any environment we haven't yet tested. A system property may be set to force running the test.


public void testRobotButtonSwapBug() throws Exception {
// skip the test if we already know the bug exists, unless forcing a bug check
if (!Boolean.getBoolean("check_all_bugs") || hasRobotButtonSwapBug()) return;
// perform tests to reproduce the bug
final List events = new ArrayList();
Robot robot = new java.awt.Robot();
Window w = new Window();
w.add(new JLabel("Swap test"));
w.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
events.add(e);
}
});
showWindow(w);
Point where = w.getLocationOnScreen();
where.x += w.getWidth()/2;
where.y += h.getWidth()/2;
robot.mouseMove(where.x, where.y);
robot.mousePress(InputEvent.BUTTON2_MASK);
robot.mouseRelease(InputEvent.BUTTON2_MASK);
robot.waitForIdle();
assertEquals("Robot generated incorrect button event for button 2",
InputEvent.BUTTON2_MASK,
((MouseEvent)events.get(0)).getModifiers());
// and so on...
}


With each new library release, the test is run. If it fails with a new version or a new platform, extra checks can be added to the method in Bugs.

The Bugs method test can now be used in client code to determine whether workarounds need to be applied. Such checking allows the client code to be used consistently regardless of the environment in use on the project where it is used. If you release an application that runs on Java 1.2 and above, you may need to selectively apply workarounds depending on the actual JRE in use by a user. In the example above, a workaround to swap button definitions was applied (and still is) if the user happens to be using the affected Java release on OSX.

This pattern makes bug checks and workarounds very explicit. Compare these two implementations. First, the ad hoc workaround:

JTree tree;
...
// tree row size isn't updated automatically, so hack it
tree.setRowSize(tree.getRowSize());
tree.revalidate();
tree.repaint();


Or this, which is only slightly more structured, but entirely clear in intent. You can also tell exactly what code can go away when the bug does:

JTree tree;
...
if (Bugs.hasTreeRowSizeUpdateBug()) {
tree.setRowSize(tree.getRowSize());
tree.revalidate();
tree.repaint();
}


The code explicitly identifies a bug that has been at least partially analyzed, and indicates workaround code. If a programmer needs further information on the bug, she can just look up the corresponding information in Bugs or BugsTest

No comments: