The Source for Java Technology Collaboration
User: Password:



Kohsuke Kawaguchi

Kohsuke Kawaguchi's Blog

Improving test harness: generating coverage report with on-the-fly instrumentation

Posted by kohsuke on September 02, 2007 at 12:06 PM | Comments (2)

Metro uses a dedicated test harness for writing end-to-end unit tests. This harness builds on top of JUnit but adds a lot of useful features and conventions so that we can write tests more productively. One of the features I added lately was the ability to generate code coverage report, and I'm going to talk about the technique because I think this is useful to other test harnesses.

The standard way to do code coverage report is by instrumenting byte code to record information. Because of this, most tools typically ask you to run a tool to post-process class files. This makes sense but it's tedious. Since Metro test harness (and most test harnesses in general) uses multiple classloaders to isolate the test subject from the harness, if we can have a custom classloader that performs instrumentation on-the-fly, then we can produce a coverage report without requiring any upfront processing.

In Metro, we use Emma for the coverage report, so the following code shows how to do this in Emma. Doing something like this with Cobertura is quite easy, except that its license GPL forces your harness to be GPL, too.

Anyway, the first you need is a ClassLoader that calls bytecode transformer in Emma. There's nothing particularly noteworthy here. You just invoke IClassLoadHook.processClassDef:

public final class InstrumentingClassLoader extends AntClassLoader2 {
    final IClassLoadHook transformer;

    public InstrumentingClassLoader(IClassLoadHook transformer) {
        this.transformer = transformer;
    }

    public final Class findClass(final String name) throws ClassNotFoundException {
        // find the class file image
        String classResource = name.replace('.', '/') + ".class";
        URL classURL = getResource(classResource);

        if (classURL == null)
            throw new ClassNotFoundException(name);

        try {
            byte[] image = readFully(classURL);

            ByteArrayOStream baos = new ByteArrayOStream(image.length);
            if (transformer.processClassDef(name, image, image.length, baos))
                image = baos.copyByteArray();

            return defineClass(name, image, 0, image.length);
        } catch (IOException ioe) {
            throw new Error(ioe);
        }
    }

    private byte[] readFully(URL resource) throws IOException {
        URLConnection con = resource.openConnection();
        InputStream in = con.getInputStream();
        try {
            int len = con.getContentLength();
            if(len!=-1) {
                byte[] buf = new byte[len];
                new DataInputStream(in).readFully(buf);
                return buf;
            } else {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte[] buf = new byte[1024];
                while((len=in.read(buf))>=0)
                    baos.write(buf,0,len);
                return baos.toByteArray();
            }
        } finally {
            in.close();
        }
    }
}

Then I wrapped the whole thing into the Emma class. IInclExclFilter is a mechanism that can be used to decide which classes to instrument and which classes not to.

public class Emma {
    private final IMetaData metadata;
    private final IInclExclFilter filter;
    private ICoverageData coverage;

    public Emma() {
        this(new IInclExclFilter() {
            public boolean included(String s) {
                return true;
            }
        });
    }

    public Emma(IInclExclFilter filter) {
        // we'll take care of record dump
        RTSettings.setStandaloneMode(false);
        RT.reset(true,false);


        // KK : not sure exactly what IProperties are abstracting,
        // but looks like they are used to allow emma configurations
        // to be given from different sources
        
        IProperties appProperties = RT.getAppProperties();
        if (appProperties == null) appProperties = EMMAProperties.getAppProperties();
        metadata = DataFactory.newMetaData(CoverageOptionsFactory.create(appProperties));

        coverage = RT.getCoverageData();

        this.filter = filter;
    }

    public AntClassLoader2 createInstrumentingClassLoader() {
        return new InstrumentingClassLoader(new InstrClassLoadHook(filter, metadata));
    }

    /**
     * Writes the coverate report as a data file.
     */
    public void write(File output) throws IOException {
        IMetaData msnap = metadata.shallowCopy();
        if (msnap.isEmpty ()) {
            System.err.println("no metadata collected at runtime [no reports generated]");
            return;
        }

        ICoverageData csnap = coverage.shallowCopy();
        if (csnap.isEmpty ()) {
            System.err.println("no coverage data collected at runtime [all reports will be empty]");
            return;
        }

        System.out.println("Writing emma coverage report");
        DataFactory.persist(new SessionData(msnap, csnap), output, false);
    }
}

The createInstrumentingClassLoader method returns a instrumenting classloader. This extends from AntClassLoader, so you can set the parent classloader and add classpaths after you create an instance.

When everything is over, you can call the write method to get the coverage data file written in a file. You can then use emma CLI/Ant tasks to produce human-readable reports.


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first)

  • So what exactly are you doing to identify test classes from your subject classes? Not every non-subject class is a part of the test harness. For instance, I usually have tests that involve a lot of stubs, mocks or other implementations of interfaces in the code I'm testing. There is no Junit parent class to look for, the packages are often the same, there really isn't anything for the classloader can use to tell the difference. It's a semantic difference a human can identifty and not one the compiler or runtime really knows.

    I'm also not sure why it's more convenient for you to do this rather than use the emma ant task to just instrument your source before your unit test and generate the report after. Seems like it's the same amount of work, except that now if something goes wrong people can't go read the emma ant task manual, they have to come and ask you about how this custom thing works.


    Posted by: scrop on September 02, 2007 at 11:00 PM


  • As for your 1st question, in our test harness we have no such issues. The test subject and other libraries (or all the generated classes) are loaded by different classloaders, so it's quite easy to distinguish them.


    But even in your case, I think one approach is to decide the instrumentation by looking at the jar file that contains the class in question.


    For your 2nd question, the difference is that there are many different situations/people who'd need to run tests, whereas I'd only have to implement this once. Our tests run against JAX-WS RI, WSIT, against JAX-WS RI in JDK. We run it from CI, during JavaSE integration, during Glassfish integration, etc., by different people.

    Posted by: kohsuke on September 02, 2007 at 11:27 PM





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds