Sunday, September 25, 2011

AST Transformations: Testing and Error Messages

When testing AST transformations we have different potential ways to do it, with respective advantages and disadvantages. In this entry we will look at direct testing, testing using a GroovyClassLoader and how global AST transformations can be tested. We also examine the last missing puzzle piece to our jigsaw puzzle of building local AST transformations, namely how to notify the user of errors in the way the transformation is used.

Direct Testing
Implementing a test directly is the simplest way to test whether a transformation does what we expect. The one disadvantage of this kind of testing is that you cannot watch the compiler do its work i.e., you cannot debug through your AST transformation (at least not without additional effort). Why? When the test class is loaded and compiled, then, in the phase "Semantic Analysis" the referenced classes are resolved, loaded, and compiled. So, when the actual test run starts, the whole compilation we are interested in has already ended. Additionally, the compiled transformation and annotation have to be on the classpath. If you are still developing the AST transformation this is not the best way to go.

Nonethelesse it is ok to simply have this kind of test in your test suite to ensure that the AST transformation still works (as a regression test). Here is an example that verifies the behavior (and yes, it is not perfect in that it does not test all edge cases).

class RequiresDirectTest extends GroovyTestCase {

    public void testInvokeUnitTest() {
        def out = new RequiresExample()

        assert out.divide10By(2) == 5

        def exceptionMessage = shouldFail { out.divide10By(0) }
        assert exceptionMessage =~ /Precondition violated/
    }
}


Testing using a Classloader
The better way to test during development is by using a GroovyClassLoader that parses the source code. Since this includes compiling the code you have every option to debug your AST transformation and watch exactly what happens.

We start by either defining a String containing the source class, or by loading the containing file from the file system. I prefer the second way, because then you can edit the class with a normal editor, and you can use the direct testing in parallel.

Then we create a new GroovyClassLoader, parse the class and create a new instance, on which we again execute our tests. And, as said above, during parseClass() the compiler is called and your AST transformation is executed during the "normal" program run.

class RequiresClassloaderTest extends GroovyTestCase {

    public void testInvokeUnitTest() {
        def file = new File('./RequiresExample.groovy')
        assert file.exists()

        GroovyClassLoader invoker = new GroovyClassLoader()
        def clazz = invoker.parseClass(file)
        def out = clazz.newInstance()

        assert out.divide10By(2) == 5

        def exceptionMessage = shouldFail { out.divide10By(0) }
        assert exceptionMessage =~ /Precondition violated/
    }
}


Global AST transformations
Global AST transformations have to be placed in the class path, in a jar with an additional text file containing the class names of the classes implementing the transformations (see references). Luckily, Hamlet D'Arcy has written a helper class that creates a new GroovyClassLoader and adds the transformation.

The interesting thing is, that this also registers a local transformation correctly. There is one caveat though: the transformation now is registered as a local and a global transformation that thus gets triggered twice. One call is as expected (the call for the local transformation), the other call (as a global transformation) provides only the source unit and no AST nodes i.e., the first argument is null. If you followed the defensive programming style, then you already check whether the AST nodes are provided and stop processing if not. This means you can use the helper class as well, and you can find many examples on the web. The question is: Should you? My opinion is that it helps if you ultimately try to create a global transformation and want to start by implementing a local transformation. In all other cases I would prefer the class loader approach.

class RequiresTransformTestHelperTest extends GroovyTestCase {

    public void testInvokeUnitTest() {
        def file = new File('./RequiresExample.groovy')
        assert file.exists()

        def invoker = new TransformTestHelper(new RequiresTransformation(),
                                              CompilePhase.CONVERSION)

        def clazz = invoker.parse(file)
        def out = clazz.newInstance()

        assert out.divide10By(2) == 5
        def exceptionMessage = shouldFail { out.divide10By(0) }
        assert exceptionMessage =~ /Precondition violated/
    }
}


Signalling Errors to the Compiler
If everything works as expected our transformation transforms the AST. But if something goes wrong we should try to communicate our problems to the user. The problem: There is no printwriter and no logger that can be used (and it wouldn't be a good way to do this anyway).

We could throw an exception or even an error, but we shouldn't do this either. Now, if you look at many of the transformations provided by Groovy, you could get the impression that throwing exceptions and errors is good manners. But the point is that these transformation, being part of Groovy, have a totally different implicit contract with the compiler. When we build an AST transformation, and some internals of the compiler on which we depend changes, than this is not the fault of the compiler but ours. So, no exceptions or errors; the compiler should execute without disruption, collecting errors from the different phases and different transformations to give the user as complete a list of the errors as it could determine.

The compiler, in fact, offers us a functionality to add our errors to a list of errors and continue the processing. Since errors are specific to a source, the SourceUnit provided as an argument to the visit()-method has an error collector that we can use to do this. The error collector can be accessed using the method getErrorCollector(), and offers a method addErrorAndContinue() that we can use. It takes an object of the class SyntaxErrorMessage, which in turn needs a SyntaxException as argument. In the following method, which can be found in the LogASTTransformation (and which is a variation of another method found in the class ClassCodeVisitorSupport), all these objects are created and fed to the error collector.

So, why not steal from the best? Here is the method, slightly modified for readability

public void addError(String msg, ASTNode expr, SourceUnit source) {
    int line = expr.lineNumber
    int col = expr.columnNumber

    SyntaxException se = new SyntaxException(msg + '\n', line, col)
    SyntaxErrorMessage sem = new SyntaxErrorMessage(se, source)
    source.errorCollector.addErrorAndContinue(sem)
}


And you simply use it like this (using our example):


public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {

    if (!checkNode(astNodes, annotationType)) {
        addError("Internal error on annotation", astNodes[0], sourceUnit);
        return
    }
    ...
}


This presents an error message to the user with line and column and, if possible, the source line where we encountered the error. Since we used the first node in the array of nodes, we associate the error with the annotation itself (the second node would be the annotated element).

Finally we can fill in those parts of the AST transformation where we had a comment "better error handling" and signal those errors to the user.

End
Now you have everything you need to successfully create your own, interesting AST transformations. I'm looking forward to a lot of new and interesting ideas being realized as AST transformations.

There is one loose end we still have to cover, though. The AstBuilder provides two additional methods for creating an AST, buildFromCode() and buildFromSpec(), which we will revisit soon. So stay tuned ...

References
Global AST Transformations Groovy documentation on global AST transformations

Sourcecode
RequiresDirectTest.groovyThe direct test
RequiresClassloaderTest.groovy    The test using a Groovy Classloader
RequiresTransformTestHelperTest.groovy    The test using a TransformTestHelper

Interesting Classes
org.codehaus.groovy.tools.ast.TransformTestHelperThe helper class that can test global AST transformations
org.codehaus.groovy.transformLogASTTransformation  The transformation containing our addError()-method
org.codehaus.groovy.ast.ClassCodeVisitorSupportThe class containing the original addError()-method


No comments: