Performance Zone is brought to you in partnership with:

Gerard Davison is a Senior Principal Software Engineer working at Oracle in the UK on SOAP and REST tooling. Currently he is contributing in the area of WADL generation and client generation in the Jersey project and is maintaining the Abbot swing automation project. He also maintain a small holding of Hudson nodes run all those tests. He graduated from the University of Reading with a degree in Human Cybernetic and can't help looking for feedback loops. Gerard is a DZone MVB and is not an employee of DZone and has posted 32 posts at DZone. You can read more from them at their website. View Full User Profile

Write an Auto-debugger to Catch Exceptions During Test Execution

10.03.2013
| 3806 views |
  • submit to reddit

Previously I have stated that there are some exceptions you would always want to keep a debugger breakpoint on for. This helps prevent code rotting away without you noticing -- but this sometimes masks a different problem.

If you take this seriously then it is a good idea to extend this idea to your automated testing, but coming up with a comprehensive solution is not entirely trivial. You could just start with a try/catch but that won't capture exceptions on other threads. You could also do something using AOP, but depending on the framework you are not guaranteed to catch everything and it does mean that you are testing with slightly different code, which will worry some.

A few days ago I came across this blog entry on how to write your own debugger, and I wondered if it was possible for a Java process to debug itself. Turns out, you can, and here is the code I came up with as part of this little thought experiment.

The first part of the class just contains some fairly hacky code to guess what port would be required to connect back to the same VM based on the start-up parameters. It might be possible to use the Attach mechanism to start the debugger, but I didn't see an obvious way to get it to work. Then there are just a couple of factory methods that take a list of exceptions to look out for.

    package com.kingsfleet.debug;  
      
    import com.sun.jdi.Bootstrap;  
    import com.sun.jdi.ReferenceType;  
    import com.sun.jdi.VirtualMachine;  
    import com.sun.jdi.connect.AttachingConnector;  
    import com.sun.jdi.connect.Connector;  
    import com.sun.jdi.connect.IllegalConnectorArgumentsException;  
    import com.sun.jdi.event.ClassPrepareEvent;  
    import com.sun.jdi.event.Event;  
    import com.sun.jdi.event.EventQueue;  
    import com.sun.jdi.event.EventSet;  
    import com.sun.jdi.event.ExceptionEvent;  
    import com.sun.jdi.event.VMDeathEvent;  
    import com.sun.jdi.event.VMDisconnectEvent;  
    import com.sun.jdi.request.ClassPrepareRequest;  
    import com.sun.jdi.request.EventRequest;  
    import com.sun.jdi.request.ExceptionRequest;  
      
    import java.io.IOException;  
      
    import java.lang.management.ManagementFactory;  
    import java.lang.management.RuntimeMXBean;  
      
    import java.util.Collections;  
    import java.util.HashSet;  
    import java.util.List;  
    import java.util.Map;  
    import java.util.Set;  
    import java.util.concurrent.CountDownLatch;  
    import java.util.concurrent.TimeUnit;  
      
      
    public class ExceptionDebugger implements AutoCloseable {  
      
      
      
      
       public static int getDebuggerPort() {  
           // Try to work out what port we need to connect to  
      
           RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();  
           List<String> inputArguments = runtime.getInputArguments();  
           int port = -1;  
           boolean isjdwp = false;  
      
           for (String next : inputArguments) {  
               if (next.startsWith("-agentlib:jdwp=")) {  
                   isjdwp = true;  
                   String parameterString = next.substring("-agentlib:jdwp=".length());  
                   String[] parameters = parameterString.split(",");  
                   for (String parameter : parameters) {  
                       if (parameter.startsWith("address")) {  
                           int portDelimeter = parameter.lastIndexOf(":");  
                           if (portDelimeter != -1) {  
                               port = Integer.parseInt(parameter.substring(portDelimeter + 1));  
                           } else {  
                               port = Integer.parseInt(parameter.split("=")[1]);  
                           }  
                       }  
                   }  
               }  
           }  
           return port;  
       }  
      
      
       public static ExceptionDebugger connect(final String... exceptions) throws InterruptedException {  
           return connect(getDebuggerPort(),exceptions);  
       }  
      
       public static ExceptionDebugger connect(final int port, final String... exceptions) throws InterruptedException {  
      
           ExceptionDebugger ed = new ExceptionDebugger(port, exceptions);  
      
           return ed;  
       }  
The constructor creates a simple daemon thread that starts the connection back to the VM. It is very important to do this in a separate thread, otherwise obviously the VM will grind to a halt when we hit a breakpoint. It is a good idea to make sure that code in that thread doesn't throw one of the exceptions -- for the moment I am just hoping for the best.

Finally, the code just maintains a list of the banned exceptions, and if you had a bit more time it should be possible to store the stack trace where the exception occurred.
    //   
       // Instance variables  
      
       private final CountDownLatch startupLatch = new CountDownLatch(1);  
       private final CountDownLatch shutdownLatch = new CountDownLatch(1);  
      
       private final Set<String> set = Collections.synchronizedSet(new HashSet<String>());  
       private final int port;  
       private final String exceptions[];  
       private Thread debugger;  
       private volatile boolean shutdown = false;  
      
      
       //  
       // Object construction and methods  
       //  
      
      
      
      
       private ExceptionDebugger(final int port, final String... exceptions) throws InterruptedException {  
      
           this.port = port;  
           this.exceptions = exceptions;  
      
           debugger = new Thread(new Runnable() {  
      
               @Override  
               public void run() {  
                   try {  
                       connect();  
                   } catch (Exception ex) {  
                       ex.printStackTrace();  
                   }  
               }  
           }, "Self debugging");  
           debugger.setDaemon(true); // Don't hold the VM open  
           debugger.start();  
      
           // Make sure the debugger has connected  
           if (!startupLatch.await(1, TimeUnit.MINUTES)) {  
               throw new IllegalStateException("Didn't connect before timeout");  
           }  
       }  
      
      
       @Override  
       public void close() throws InterruptedException {  
           shutdown = true;  
           // Somewhere in JDI the interrupt was being eaten, hence the volatile flag   
           debugger.interrupt();  
           shutdownLatch.await();  
       }  
      
      
       /** 
        * @return A list of exceptions that were thrown 
        */  
       public Set<String> getExceptionsViolated() {  
           return new HashSet<String>(set);  
       }  
      
       /** 
        * Clear the list of exceptions violated 
        */  
       public void clearExceptionsViolated() {  
           set.clear();  
       }  
The main connect method is a fairly simple block of code that ensures the connection and configures any initial breakpoints.
    //  
       // Implementation details  
       //  
      
      
       private void connect() throws java.io.IOException {  
      
      
           try {  
               // Create a virtual machine connection  
               VirtualMachine attach = connectToVM();  
      
      
               try  
               {  
      
                   // Add prepare and any already loaded exception breakpoints  
                   createInitialBreakpoints(attach);  
      
                   // We can now allow the rest of the work to go on as we have created the breakpoints  
                   // we required  
      
                   startupLatch.countDown();  
      
                   // Process the events  
                   processEvents(attach);  
               }  
               finally {  
      
                   // Disconnect the debugger  
                   attach.dispose();  
      
                   // Give the debugger time to really disconnect  
                   // before we might reconnect, couldn't find another  
                   // way to do this  
      
                   try {  
                       TimeUnit.SECONDS.sleep(1);  
                   } catch (InterruptedException e) {  
                       Thread.currentThread().interrupt();  
                   }  
               }  
           } finally {  
               // Notify watchers that we have shutdown  
               shutdownLatch.countDown();  
           }  
       }  
Connecting back to self is just a process of finding the right attaching connector, in this case Socket, although I guess you could use the shared memory transport on some platforms if you modified the code slightly.
    private VirtualMachine connectToVM() throws java.io.IOException {  
      
           List<AttachingConnector> attachingConnectors = Bootstrap.virtualMachineManager().attachingConnectors();  
           AttachingConnector ac = null;  
      
           found:  
           for (AttachingConnector next : attachingConnectors) {  
               if (next.name().contains("SocketAttach")) {  
                   ac = next;  
                   break;  
      
               }  
           }  
      
           Map<String, Connector.Argument> arguments = ac.defaultArguments();  
           arguments.get("hostname").setValue("localhost");  
           arguments.get("port").setValue(Integer.toString(port));  
           arguments.get("timeout").setValue("4000");  
      
           try {  
               return ac.attach(arguments);  
           } catch (IllegalConnectorArgumentsException e) {  
               throw new IOException("Problem connecting to debugger",e);  
           }  
       }  
When you connect the debugger you have no idea as to whether the exceptions you are interested in have been loaded, so you need to register breakpoints for both the point where the classes are prepared and for those that have already been loaded.

Note that the breakpoint is set with a policy only to break the one thread, otherwise for obvious reasons the current VM will grind to a halt if the debugger thread is also put to sleep.
    private void createInitialBreakpoints(VirtualMachine attach) {  
           // Our first exception is for class loading  
      
           for (String exception : exceptions) {  
               ClassPrepareRequest cpr = attach.eventRequestManager().createClassPrepareRequest();  
               cpr.addClassFilter(exception);  
               cpr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);  
               cpr.setEnabled(true);  
           }  
      
           // Then we can check each in turn to see if it have already been loaded as we might  
           // be late to the game, remember classes can be loaded more than once  
           //  
      
           for (String exception : exceptions) {  
               List<ReferenceType> types = attach.classesByName(exception);  
               for (ReferenceType type : types) {  
                   createExceptionRequest(attach, type);  
               }  
           }  
       }  
      
      
       private static void createExceptionRequest(VirtualMachine attach,   
                                                  ReferenceType refType) {  
           ExceptionRequest er = attach.eventRequestManager().createExceptionRequest(  
               refType, true, true);  
           er.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);  
           er.setEnabled(true);  
       }  
The event processing loop polls for EventSet instances that contain one or more Event instances. Not all of these events are down to a breakpoint request, though, so you have to take care to not always call resume on the event set. This is because you might have two event sets in a row, with the code calling resume before you even get to read the second one. This results in missed breakpoints as the code catches up.

For some reason JDI appeared to be eating the interrupted flag, hence the Boolean property to stop the loop with the close method from before.
    private void processEvents(VirtualMachine attach) {  
           // Listen for events  
      
           EventQueue eq = attach.eventQueue();  
           eventLoop: while (!Thread.interrupted() && !shutdown) {  
      
               // Poll for event sets, with a short timeout so that we can  
               // be interrupted if required  
               EventSet eventSet = null;  
               try   
               {  
                   eventSet = eq.remove(500);  
               }  
               catch (InterruptedException ex) {  
                   Thread.currentThread().interrupt();  
                   continue eventLoop;    
               }  
      
               // Just loop again if we have no events  
               if (eventSet == null) {  
                   continue eventLoop;  
               }  
      
               //  
      
               boolean resume = false;  
               for (Event event : eventSet) {  
      
                   EventRequest request = event.request();  
                   if (request != null) {  
                       int eventPolicy = request.suspendPolicy();  
                       resume |= eventPolicy != EventRequest.SUSPEND_NONE;  
                   }  
      
                   if (event instanceof VMDeathEvent || event instanceof VMDisconnectEvent) {  
                       // This should never happen as the VM will exit before this is called  
      
                   } else if (event instanceof ClassPrepareEvent) {  
      
                       // When an instance of the exception class is loaded attach an exception breakpoint  
                       ClassPrepareEvent cpe = (ClassPrepareEvent) event;  
                       ReferenceType refType = cpe.referenceType();  
                       createExceptionRequest(attach, refType);  
      
                   } else if (event instanceof ExceptionEvent) {  
      
                       String name = ((ExceptionRequest)event.request()).exception().name();  
                       set.add(name);  
                   }  
               }  
      
               // Dangerous to call resume always because not all event suspend the VM  
               // and events happen asynchornously.  
               if (resume)  
                   eventSet.resume();  
           }  
       }  
      
    }  
So all that remains is a simple test example, since this is JDK 7 and the ExceptionDebugger is AutoCloseable we can do this using the try-with-resources construct as follows. Obviously, if doing automated tests, use the testing framework fixtures of your choice.
    public class Target {  
      
       public static void main(String[] args) throws InterruptedException {  
      
      
           try (ExceptionDebugger ex = ExceptionDebugger.connect(  
                   NoClassDefFoundError.class.getName())) {  
      
               doSomeWorkThatQuietlyThrowsAnException();  
      
               System.out.println(ex.getExceptionsViolated());  
           }  
      
      
           System.exit(0);  
       }  
      
      
       private static void doSomeWorkThatQuietlyThrowsAnException() {  
           // Check to see that break point gets fired  
      
           try {  
               Thread t = new Thread(new Runnable() {  
                               public void run() {  
                                   try  
                                   {  
                                       throw new NoClassDefFoundError();  
                                   }  
                                   catch (Throwable ex) {  
      
                                   }  
                               }  
                          });  
               t.start();  
               t.join();  
           } catch (Throwable th) {  
               // Eat this and don't tell anybody  
           }  
       }  
    }  
So if you run this class with the following VM parameter, note the suspend=n otherwise the code won't start running, you will find that it can connect back to itself and start running.
-agentlib:jdwp=transport=dt_socket,address=localhost:5656,server=y,suspend=n
This gives you the following output, note the extra debug line from the VM:
Listening for transport dt_socket at address: 5656
[java.lang.NoClassDefFoundError]
Listening for transport dt_socket at address: 5656
As always, I would be interested to read if this was something that is useful for people and to help remove any obvious mistakes.



Published at DZone with permission of Gerard Davison, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)