08 Mar 05

Ant 1.6 Goody: <macrodef>

Rolling on with our tour of new features in Ant 1.6, let’s look at another new task that helps reduce duplication and promotes consistency across projects. The <macrodef> task lets you create a custom task composed of existing tasks, and specify custom parameters to those tasks on a case-by-case basis. Think of it as Ant’s way of letting you write reusable methods in your build files.

Say, for example, your build file includes the following test target that taste-tests the build:

  <target name="test" depends="compile-tests">
    <junit haltonfailure="true" fork="false">
      <classpath refid="project.classpath" />
      <formatter type="brief" usefile="false" />
      <batchtest>
        <fileset dir="${build.test.dir}"
                 includes="**/*Test.class" />
      </batchtest>
    </junit>
  </target>

The <batchtest> task creates a test suite containing the tests returned by the <fileset> element—all the tests defined in *Test.class files. The resulting suite is then run by the enclosing <junit> task. Once you get all the elements and attributes set the way you like, it works like a charm.

Tomorrow you’re writing a new JUnit test, but it’s not the sort of quick unit test that a programmer on a deadline would want to run frequently as part of the build process. No, your new test belongs in a different test suite than the quick tests. So you decide that you need a new Ant target called test-database that runs all of the database-related tests. That target will call all the same tasks as the test target, but with three differences: 1) the <fileset> will collect only the database tests 2) the value of the fork attribute will be set to true and 3) the database tests require system properties to be set.

It’s tempting to copy the test target and give it a slight makeover, but you know duplication is just a shortcut to madness. Rather, extract out the commonality into a <macrodef> task:

  <macrodef name="run-tests">
    <attribute name="classes" />
    <attribute name="fork" default="false" />
    <element name="options" optional="true" />
    <sequential>
      <junit haltonfailure="true" fork="@{fork}">
        <classpath refid="project.classpath" />
        <formatter type="brief" usefile="false" />
        <batchtest>
          <fileset dir="${build.test.dir}"
                   includes="@{classes}" />
        </batchtest>
        <options />
      </junit>
    </sequential>
  </macrodef>

Notice that the run-tests macro has two attributes: classes and fork. When you call the macro, you must provide a value for classes. The fork attribute, if not specified, has a default value of false. The real work happens in the sequential task, composed of the same tasks as your original test target. The difference here is instances of @{fork} and @{classes} are replaced with their corresponding attribute values. Additionally, the <options> element is replaced with the elements enclosed in an options element, if specified when the macro is called.

Now change the test target to simply call your run-tests macro:

  <target name="test" depends="compile-tests">
    <run-tests classes="**/*Test.class" />
  </target>

Note that these tests run with a fork attribute value of false and without any options. That is, the default attribute values are used and the optional elements aren’t provided. Ah… less XML for you to type and maintain.

Finally, define a new target that calls the run-tests macro to run just the database tests:

  <target name="test-database" depends="compile-tests">
    <run-tests classes="**/TestDB.class" fork="yes">
      <options>
        <sysproperty key="db.url" value="${db.url}" />
        <sysproperty key="db.user" value="${db.user}" />
        <sysproperty key="db.password" value="${db.password}" />
      </options>
    </run-tests>
  </target>

In this case, the fork attribute is turned on and a few system properties are provided. (Note that any element that can be nested inside of a <junit> task can be specified in the <options> element.)

Extracting the common tasks and defaults out into the run-tests macro removed duplication, which means we can change the steps of the testing process in one location: the run-tests macro. This works well for common build targets that are complex in that they include several tasks and/or use several properties, such as the <javac> task. By comparison, the new <presetdef> task is similar to <macrodef>, but it doesn’t allow custom parameters to be specified. In other words, if <macrodef> is like a Java method that takes arguments, <presetdef> is like a no-arg method.

Combined with the new <import> task we visited last time, use of the <macrodef> task leads to shorter build files and consistency across projects. And by writing macros that have intention-revealing names and use intelligent defaults, your build files start to read more like a concise build language.

There’s more to come, so stay tuned for the next new feature…