Writing a plugin

In a previous article you have been guided to write project specific logic to implement a new feature in easyant : code coverage.
We will see here how to extract this specific logic into a reusable easyant plugin.

Generating plugin from a skeleton

First, we need to create a new plugin structure. To ease plugin development easyant came with a skeleton for plugins.
Skeletons plugin works by default in interractive mode, so it will ask a few questions.

> easyant skeleton:newplugin
-skeleton:check-generate:
...
[input] The path where the skeleton project will be unzipped [.]

[input] Organisation name of YOUR project [org.apache.easyant.plugins]

[input] Module name of YOUR project
cobertura
[input] Revision number of YOUR project [0.1]

We choose default value between brackets except for the module name.
For maven users :

  • organisation = groupId
  • module = artifactId
  • revision = version

We’ve a ready to use plugin structure. Plugin structure is very similar to a standard easyant project scructure with :

  • a module ivy file containing revision number, description and plugin dependencies
  • src/main/resources containing the plugin logic (.ant files but also a good place to put additionnal files required by the plugin)
  • src/test/antunit containing plugin tests
|-- module.ivy
`-- src
|-- main
|   `-- resources
|       `-- cobertura.ant
|-- test
     `-- antunit
        `-- cobertura-test.xml

Ant file

The skeleton has generated the plugin main file in src/main/resources/cobertura.ant

<project name="org.apache.easyant.plugins;cobertura"
xmlns:ivy="antlib:org.apache.ivy.ant"
xmlns:ea="antlib:org.apache.easyant">

  <!-- Force compliance with easyant-core to 0.7 or higher -->
  <!-- <ea:core-version requiredrevision="[0.7,+]" /> -->

  <!-- Sample init target -->
  <target name="cobertura:init">
    <!-- you should remove this echo message -->
    <echo level="debug">This is the init target of cobertura</echo>
  </target>
</project>

By convention, projectname of the plugin should be formed like

[organisation];[module]

Example:

org.apache.easyant.plugins;cobertura

Understanding extension-point

Extension-points are plugins hooks.  Plugins typically add low-level tasks to one or more extension-points. For example, a plugin can contribute to processing sources before compilation, you will in that case plug your own target to “abstract-compile:compile-ready” extension-point”. This plugin hooks is defined in abstract-compile plugin”.

So we need to import this plugin and plug our own target on it.

<ea:plugin module="abstract-compile" revision="0.9"/>
<target name="cobertura:mytarget" extensionOf="abstract-compile:compile-ready">
...//your stuff here
</target>

Less typically, a plugin can also define new extension-points for other plugins to use. We highly recommend in that case to create an “abstract” plugin defining common stuff and extension-points to limit coupling between plugins and make them more flexible.

In standard build types the project-lifecycle is defined by a plugin named phases-std. This plugin loads the default lifecycle containing a set of high level extensionPoints like compile,package.
It’s build types responsability to import this plugin and and do the binding between targets and extension-points from this lifecycle.

Target Naming Conventions

By default, all targets should be prefixed by plugin name. In our example “init” target is prefixed by “cobertura”.

There is a conventional difference in the way public and private targets are named in Easyant. A public target is one that makes sense for the end user to be aware of, while a private target should be hidden from the end user.

Conventionally,

  • a public target should always have an associated ‘description’ attribute.
  • a private target should begin with a “-”

Those conventions are not mandatory. They just ease understanding.

Example :

<target name="cobertura:helloworld" depends="cobertura:init" description="display an hello world">
  <echo>hello world !</echo>
</target>

<target name="cobertura:hello" depends="cobertura:init,-check-username" description="display an hello to current user">
  <echo mess="Hello ${username}"/>
</target>

Whereas a private target name should begin with ‘-’.

Example :

<!-- this target initialize username property if it's not already set -->
<target name="-cobertura:check-username" unless="username">
  <echo>You can also add a "-Dusername=YOU" on the commandline to display a more personal hello message</echo>
  <property name="username" value="${user.name}"/>
</target>

Implementing the logic

It is now time to extract cobertura targets from project specific logic in a plugin.

<target name="cobertura:init">
  <ivy:cachepath pathid="cobertura.classpath" inline="true" organisation="net.sourceforge.cobertura" module="cobertura" revision="1.9.4.1" conf="default" settingsRef="easyant.ivy.instance"/>
  <taskdef classpathref="cobertura.classpath" resource="tasks.properties" />
</target>

<target name="cobertura:instrument" depends="cobertura:init,compile,abstract-test:init">
  <property name="coverage.dir" value="${target}/coverage"/>
  <property name="coverage.datafile" value="${coverage.dir}/cobertura.ser"/>
  <mkdir dir="${coverage.dir}"/>

  <!-- delete previous coverage data file -->
  <delete file="${coverage.datafile}" />

  <!-- do instrumentation -->
  <cobertura-instrument todir="${coverage.dir}" datafile="${coverage.datafile}">
    <fileset dir="${target.test.classes}" erroronmissingdir="false"/>
    <fileset dir="${target.test.integration.classes}" erroronmissingdir="false"/>
  </cobertura-instrument>
  <!-- contribute to test runtime classpath and prepend instrumented classes -->
  <ea:path pathid="run.test.classpath" overwrite="prepend">
    <fileset dir="${coverage.dir}"/>
  </ea:path>
</target>

<target name="cobertura:run" depends="cobertura:instrument">
  <cobertura-report format="html" destdir="${target.report.dir}" srcdir="${src.main.java}" datafile="${coverage.datafile}"/>
</target>

Import existing plugins

Cobertura plugin needs a few properties and classpaths defined in abstract plugins. Reusing exist stuff (properties, classpath, targets, etc…) avoids reinventing the wheel on each plugin and limit coupling two concrete plugins.
This can be achieved by import plugins like this :

<ea:plugin module="abstract-test" revision="0.9"/>
<ea:plugin module="abstract-compile" revision="0.9"/>

Reusing existing properties

Invoking ProjectMan command allow you to see properties available :

> easyant -listProps

To have only properties of a given plugin you could invoke

> easyant -listProps abstract-compile

To get more details on a given property you could invoke describe man command

> easyant -describe target.test.classes

This commands tell us it’s a property withs it’s description, default value and lot of interesting informations.

	No extension point found for name: target.test.classes
	No Target found for name: target.test.classes
	Property: target.test.classes
		Description: destination directory for compiled test classes
		Default: ${target}/test/classes
		Required: false
		Current value: ${target}/test/classes
		Defined in: 
	No Parameter found for name: target.test.classes

Reusing existing extension points

To limit coupling with default lifecycle we should not directly rely on compile phase. Instead we highly encourages you to plug your stuff into existing extension points from other plugins.
Invoking ProjectMan command allow you to see extension points available :

> easyant -listExtensionPoints

To have only extension points of a given plugin you could invoke

> easyant -listExtensionPoints abstract-compile

Let’s update target dependencies from :

<target name="cobertura:instrument" 
    depends="cobertura:init,compile,abstract-test:init"> 

to

<target name="cobertura:instrument" 
    depends="cobertura:init,abstract-compile:compile,abstract-test:init"> 

Pre conditions

A build module should always check that a set of pre conditions is met.
This can be done at the root level of your plugin or in a target. We encourage you to use a target for initialisation as you can control when it should be executed. If intialisation is done at the root level it will be executed when the plugin is loaded.

By convention, the initialisation target should be named “<plugin>:init”.

Pre conditions, including for example – checking the existence of a file or a directory, could be performed inside this target. Additionally, this target is a great place to do global initializations that are needed for the rest of the build. This could include a taskdef initialization.
Pre conditions can be performed by using the parameter task.
Let’s refactor our plugin and document properties

<ea:parameter property="cobertura.instrumented.classes.dir" 
    description="Specify the output directory for the instrumented classes" 
    default="${target}/coverage"/>
<ea:parameter property="cobertura.datafile" 
    description="Specify the name of the file to use for storing the metadata about your classes. This is a single file containing serialized Java classes. It contains information about the names of classes in your project, their method names, line numbers, etc. It will be updated as your tests are run, and will be referenced by the Cobertura reporting command." 
    default="${cobertura.instrumented.classes.dir}/cobertura.ser"/>
<ea:parameter property="cobertura.include.classes.regex"
    description="Include regex pattern to select files you want to be instrumented"
    default=".*"/>
<ea:parameter property="cobertura.exclude.classes.regex" 
    description="Exclude regex pattern to ignore files to be instrumented" 
    default=".*\.Test.*"/>
<ea:parameter property="target.reports" 
    description="base directory for reports"    
    default="${target}/reports"  />
<ea:parameter property="target.coverage.reports" 
    default="${target.reports}/coverage" 
    description="base directory where coverage reports will be generated" />
<ea:parameter property="cobertura.src.dir"
    default="${src.main.java}" 
    description="directory containing main sources (used for reports)" />

Elements defined through ea:parameter :

  • are documented
  • preconfigured with default values
  • can be required if they have no default values (useful if you need user input). If not present required property will fail the build and show parameter description.
  • will be used by ProjectManCommands (kind of interractive help on command line)
  • will soon be used by others tools (like IDE :))

What should be documented

As the plugin may be used in many different ways we need to document following elements :

  • public targets / extension points descriptions
  • parameters (properties, resource collections, paths).  For each parameter specify name, description, whether it is required, and optionally a default value. This should be done with the parameter task
  • expected environment (files in a directory, a server up and running, …)
  • results produced

Adding plugin dependencies

Most of the time when we write plugins we want to use third party ant tasks.
Here we need to depend on cobertura.jar.

Since we are now writing a plugin with it’s own module.ivy file we could remove the ivy:cachepath task in cobertura:init and add it as a dependency :

<dependencies>
  <dependency org="net.sourceforge.cobertura" name="cobertura" rev="1.9.4.1" />
</dependencies>

Using dependency in your plugin ant script

Easyant automatically creates a classpath specific for each plugin, this classpath contains all the required dependency .jars

The classpath is named by conventions

[organisation]#[module].classpath

Example:

org.apache.easyant.plugins#cobertura.classpath

Since this classpath is auto-created you can use it to reference your taskdef.


<taskdef resource="tasks.properties" classpathref="org.apache.easyant.plugins#cobertura.classpath" />

Compatibilty with core revision

A module can be dependent on features available in Easyant core. As such, it is possible for a module to be functional with particular versions of Easyant only.
Easyant provides a way for modules to explicitly specify their dependency on core revisions.
A module may use the ea:core-version task to specify such a dependency.
A task may depend on:

  • static version (Example : 0.5)
  • dynamic version (Example : latest.revision) even if we do not recommand to use it
  • listed version (Example : (0.1,0.3,0.5) )
  • range version (Example : [0.5,0.8] means from 0.5 to 0.8. Example2 : [0.5,+] means all version superior to 0.5)
<!-- Force compliance with easyant-core to 0.9 or higher -->
<ea:core-version requiredrevision="[0.9,+]" />

Publishing your plugin

You can easily publish your plugin to an easyant repository using the standard phases publish-shared (for snapshot) or release

>  easyant publish-local
>  easyant publish-shared
>  easyant release

By default plugins are published to a repository named easyant-shared-modules stored in $USER_HOME/.easyant/repository/easyant-shared-modules/.

You can specify the repository name using one of the following property

  • release.resolver
  • snapshot.resolver
Note: Repository must exist in easyant ivy instance. See configure easyant ivy instance man page for more informations.

Using your plugin in your project

Considering that you published your plugin in a easyant repository, you could use it in your project.

<ivy-module version="2.0" xmlns:ea="http://www.easyant.org">
    <info organisation="org.mycompany" module="myproject" 
            status="integration" revision="0.1">
        <ea:build module="build-std-java" revision="0.9">
            <ea:plugin organisation="org.apache.easyant.plugins" module="cobertura" revision="0.9"/>
        </ea:build>
    </info>
    <publications>
                <artifact name="cobertura" type="ant"/>
        </publications>
</ivy-module>

And now running

> easyant -p 

We should see cobertura’s target.

Main targets:
...
 cobertura:run                   generate cobertura reports
...

Writing plugin test case

By default the skeleton has generated a antunit test file in src/test/antunit/[module]-test.ant.

So in our case let’s open “src/test/antunit/cobertura-test.xml”

<project name="org.apache.easyant.plugins;cobertura-test" 
    xmlns:au="antlib:org.apache.ant.antunit"
    xmlns:ivy="antlib:org.apache.ivy.ant"
    xmlns:ea="antlib:org.apache.easyant">

    <!-- Import your plugin -->
    <property name="target" value="target/test-antunit"/>
    <!-- configure easyant in current project -->
    <ea:configure-easyant-ivy-instance / >
    <!-- import our local plugin -- >
    <ea:import-test-module moduleIvy="../../../module.ivy" sourceDirectory="../../main/resources"/ >
    
    <!-- Defines a setUp / tearDown (before each test) that cleans the environment -- > 
    <target name="clean" description="remove stale build artifacts before / after each test" >
        <delete dir="${basedir}" includeemptydirs="true" >
            <include name="**/target/**"/ >
            <include name="**/lib/**"/ >
        </delete >
    </target >
    
    <target name="setUp" depends="clean"/ >
    <target name="tearDown" depends="clean"/ >
    
    <!-- init test case -- >         
    <target name="test-cobertura:init" depends="cobertura:init">
        <au:assertLogContains level="debug" text="This is the init target of cobertura"/>
    </target>
    
</project>   

Here we :

  • configure easyant for test
  • import the plugin
  • define a generic tearDown, setUp method that cleans the target and lib directories.
  • define a test case for the init target that check that the output log contains “This is the init target of cobertura”

All targets prefixed by “test” will be executed as a test case (similar to junit 3 behavior).

Now we will write a test case for our “cobertura:init” target.

<target name="test-cobertura:init" depends="cobertura:init">
    <au:assertLogContains text="hello world !"/>
    <au:assertPropertyEquals name="cobertura.instrumented.classes.dir"
      value="${target}/coverage"/>
    <au:assertPropertyEquals name="cobertura.datafile" 
      value="${cobertura.instrumented.classes.dir}/cobertura.ser"/>
    <au:assertPropertyEquals name="cobertura.include.classes.regex" 
      value=".*"/>
    <au:assertPropertyEquals name="cobertura.exclude.classes.regex" 
      value=".*\.Test.*"/>
    <au:assertPropertyEquals name="target.reports" 
      value="${target}/reports"/>
    <au:assertPropertyEquals name="target.coverage.reports" 
      value="${target.reports}/coverage"/>
    <au:assertPropertyEquals name="cobertura.src.dir" 
      value="${src.main.java}"/>
</target>

Tests can be executed by running :

> easyant test

You can access test-reports at “target/antunit/html/index.html” or if you prefer the raw XML result “target/antunit/xml/TEST-src.test.antunit.cobertura-test_xml.xml”.

Ideally tests should emulate a fake project and verify cobertura reports are properly generated. But we will not cover it on this article.

The code of this plugin on GitHub.

3 Responses to “Writing a plugin”


  1. Jérôme

    Hello,

    I have downloaded your example from GitHub and tested it with easyant 0.9 and ant 1.8.2. I have an error when calling “easyant publish-local” or “easyant publish-shared” during ivy deliver. I use default ivy settings files provided by easyant.

    [easyant-configure-easyant] :: Apache Ivy 2.3.0 – 20130110142753 :: http://ant.apache.org/ivy/ ::
    [easyant-configure-easyant] :: loading settings :: url = jar:file:/home/jleroux/bin/easyant-core-0.9/lib/easyant-core.jar!/org/apache/easyant/core/default-easyant-ivysettings.xml
    Loading System Plugins…
    :: loading settings :: url = jar:file:/home/jleroux/bin/easyant-core-0.9/lib/ivy.jar!/org/apache/ivy/core/settings/ivysettings.xml
    [easyant-load-module] Loading build module : /home/jleroux/Documents/projects/easyant/workspace/tests/cobertura-easyant-plugin-master/module.ivy
    [echo] Building org.apache.easyant.plugins cobertura with org.apache.easyant.buildtypes#build-std-ant-plugin…
    Trying to override old definition of task check-version-number

    ivy-publication:publish-local:
    [ivy:publish] :: delivering :: org.apache.easyant.plugins#cobertura;0.1 :: 0.1-local-20131128093039 :: integration :: Thu Nov 28 09:30:39 CET 2013

    BUILD FAILED
    Dr Myrmex found an error when building cobertura
    * Where

    File : /home/jleroux/.easyant/easyant-cache/org.apache.easyant.plugins/ivy-publication/ants/ivy-publication-0.9.ant
    Line : 122 column : 90

    * Dr Myrmex diagnostic

    Error : org.apache.easyant.plugins#cobertura;0.1:
    Cause : Ivy file not found in cache for org.apache.easyant.plugins#cobertura;0.1!

    * Recomendation …

    Dr Myrmex suggest you to run easyant with -verbose or -debug option to get more details.

  2. Jean-Louis Boudart

    Hi Jérôme,

    Looks like a bug in build-std-ant-plugin buildtype.
    The error occurs because “ivy-provisioning:resolve” is not invoked.

    Could you try invoking “easyant ivy-provisioning:resolve publish-local”.

    A new version of the buildtype will be available soon.

  3. Jérôme

    Hi Jean-Louis,

    Yes, this was the issue! Thanks V_V

    BUILD SUCCESSFUL



Social Widgets powered by AB-WebLog.com.