vendredi 24 octobre 2014

Tester avec Maven un processeur d'annotation

Générer du code est une pratique courante en Java qui permet d'éviter de se répéter : il faut reconnaître que c'est une pratique bien pratique.

Mais lorsqu'il s'agit de créer un projet sous Maven avec un générateur de code (et de ressources) basé sur des annotations et qu'on envisage de tester le code généré (et les ressources), c'est une toute autre paire de manche : j'ai suffisamment lutté avec Maven pour trouver la configuration simple mais qui marche, que j'ai la joie de partager avec toi, ami lecteur.


Nous allons voir pas à pas dans cet article comment configurer notre POM pour que notre processeur d'annotation que nous développons puisse être testé sans heurts.

Le processeur d'annotation


La conception d'un processeur d'annotation n'est pas traité par cet article. Pour illustrer notre propos, prenons un générateur d'annotation déjà écrit ; en fait, prenons-en deux :
  1. LookupKeyProcessor qui prend en entrée chaque annotation @LookupKey pour générer la ressource META-INF/xservices/[forClass]/[variant] qui contient juste le nom d'une classe.
  2. LookupKeyProducerGenerator qui prend en entrée chaque annotation @Injection.Producer pour générer une classe Java à partir d'un template
Il n'est pas utile de comprendre les tenants et les aboutissants de ces deux processeurs, il faut simplement garder à l'esprit que le premier génère une ressource, et que le second génère une classe.

Mais si le coeur vous en dit, vous pouvez toujours consulter les sources du projet Alternet Tools, et la documentation qui explique en détail ce que font ces processeurs d'annotation. En quelques mots, ce projet revisite la notion de Service Locator, afin de fournir diverses variantes d'implémentations d'un service, tout en fournissant le nécessaire pour invoquer de tels services à partir d'un framework d'injection compatible JSR 330.

Nous disposons donc de nos deux processeurs d'annotation, et pour qu'ils puissent être opérationnels sans configuration de la part de l'utilisateur, il suffit de les empaqueter dans leur Jar avec la ressource :
src/main/resources/META-INF/services/javax.annotation.processing.Processor
qui contient simplement les noms de ces deux classes (il faut une ligne par processeur) :
ml.alternet.discover.gen.LookupKeyProcessor
ml.alternet.discover.gen.LookupKeyProducerGenerator
Le décor est planté.

Pour la suite, vous ne vous positionnerez pas en tant qu'utilisateur d'une telle librairie, mais bien en tant que concepteur.
De ce point de vue, il vous faut :
  • compiler le projet
  • tester les processeurs en leur faisant générer des fichiers de ressources et des sources Java
  • compiler les sources générées Java de test
  • faire en sorte que vos tests puissent utiliser les classes et les ressources générées

Compilation du projet


Il va falloir altérer les cycles de compilation par défaut prévus par Maven. Pour cela, commencez par créer le nécessaire dans le POM :
<build>
  <plugins>
    <plugin>
      <artifactId>maven-compiler-plugin</artifactId>
      <executions>
        <!--les exécutions ici-->
      <executions>
    </plugin>
  </plugin>
</build>
Consultez le POM effectif :
# mvn help:effective-pom
et repérez le plugin maven-compiler-plugin ; notez les 2 sections d'exécution dont les ID sont :
  • default-compile
  • default-testCompile
Recopiez la première section dans la section exécution de votre POM avec la modification suivante :
    <execution>
      <id>default-compile</id>
      <phase>compile</phase>
      <goals>
        <goal>compile</goal>
      </goals>
      <configuration>
      <!-- disable processing because the definition 
           in META-INF/services breaks javac -->
        <compilerArgument>-proc:none</compilerArgument>
      </configuration>
    </execution>
A la compilation, le service défini dans META-INF sera copié dans le répertoire target du projet avec les classes compilées, mais pour que le compilateur ne plante pas, il faut lui indiquer l'argument de compilation -proc:none. Sans cela, le compilateur tentera d'appliquer le processeur indiqué dans le META-INF alors que celui-ci n'est pas encore compilé.

Autant, nous ne souhaitons pas que ce processeur soit activé pendant la compilation du projet, autant, une fois qu'il est compilé, nous souhaitons qu'il soit activé pendant la compilation des tests, puisqu'ils sont susceptibles de l'utiliser. C'est précisément ce que fait la seconde section du POM effectif, celle dédiée à la compilation des tests.

Pourtant, ça ne marche pas. Il manque encore des trucs.

Compilation des classes de test générées


Il faut indiquer que le répertoire dans lequel les sources des classes java générées sont à prendre en compte lors de la compilation :
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>build-helper-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>add-test-source</id>
      <phase>generate-test-sources</phase>
      <goals>
        <goal>add-test-source</goal>
      </goals>
      <configuration>
      <sources>
        <source>${project.build.directory}/generated-test-sources/test-annotations</source>
      </sources>
      </configuration>
    </execution>
  </executions>
</plugin>
Mais ce n'est pas suffisant ; il semble que lors de la compilation, ce répertoire étant vide il n'est pas pris en compte (pour se rendre compte que tout fonctionne tel qu'indiqué, le plus sûr est de lancer un clean préalable sur le projet, et d'aller jusqu'aux tests, à savoir mvn clean test).

La solution que j'ai trouvé à ce défaut, est de relancer une compilation pour que les sources Java générées soient compilées :
    <execution>
      <id>testCompile-generated</id>
      <phase>test-compile</phase>
      <goals>
        <goal>testCompile</goal>
      </goals>
      <configuration>
        <compilerArgument>-proc:none</compilerArgument>
      </configuration>
    </execution>
Notez que j'ai bien un ID testCompile-generated différent de celui par défaut default-testCompile qui est donc conservé et appliqué, sans quoi je surchargerai cette exécution ; cette seconde compilation n'a pas besoin du processeur d'annotation que je désactive comme précédemment avec -proc:none.

Mais ce n'est pas fini.

Prise en compte des ressources de tests générés


Il est aussi nécessaire, dans le cas ou vos artéfacts générés ne sont pas seulement des classes Java mais aussi des ressources, les copier dans le répertoire de test, de sorte qu'on puisse les trouver lors des tests :
<plugin>
  <artifactId>maven-resources-plugin</artifactId>
  <version>${version.maven-resources-plugin}</version>
  <executions>
    <execution>
      <id>copy-annotations</id>
      <phase>process-test-classes</phase>
      <goals>
        <goal>copy-resources</goal>
      </goals>
      <configuration>
        <outputDirectory>${project.build.directory}/test-classes</outputDirectory>
        <resources>
          <resource>
            <directory>${project.build.directory}/generated-test-sources/test-annotations</directory>
            <excludes>
              <exclude>**/*.java</exclude>
            </excludes>
          </resource>
        </resources>
      </configuration>
    </execution>
  </executions>
</plugin>
Contrairement à ce qu'on pourrait croire, c'est bien à la phase "process-test-classes" que doit être attachée la copie, et non pas "process-test-resources" puisque la compilation se passe après cette dernière phase (et non pas lors de la désormais mal-nommée phase "generate-test-resources", vraisemblablement en vigueur lorsque APT était un outil séparé de javac) ; cela se vérifie dans les traces avec un mvn clean test.

Après cela, les tests qui utilisent des annotations qui provoquent la génération de code Java et de ressources additionnelles peuvent vérifier que ce qui a été généré fonctionne.

Dans le projet Alternet Tools, les tests sont ici, et les rapports d'exécution ici.


Utilisation dans Eclipse


Pour les utilisateurs d'Eclipse, vous aurez la désagréable surprise de constater une erreur au niveau de l'exécution du plugin build-helper-maven-plugin :
Plugin execution not covered by lifecycle configuration: org.codehaus.mojo:build-helper-maven-plugin:1.9:add-test-source
(execution: add-test-source, phase: generate-test-sources)
Grosso-modo, Eclipse m2e se plaint de ne savoir que faire de ce plugin. Il faut lui indiquer un "mapping", et comme les choses sont mal faites, ce n'est pas un paramétrage propre à Eclipse qui permet d'indiquer cela, mais l'ajout d'une pollution dans le POM :
<pluginManagement>
  <plugins>
    <plugin>
      <groupId>org.eclipse.m2e</groupId>
      <artifactId>lifecycle-mapping</artifactId>
      <version>1.0.0</version>
      <configuration>
        <lifecycleMappingMetadata>
          <pluginExecutions>
            <pluginExecution>
              <pluginExecutionFilter>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>build-helper-maven-plugin</artifactId>
                <versionRange>[1.0.0,)</versionRange>
                <goals>
                  <goal>add-test-source</goal>
                  <goal>add-test-resource</goal>
                </goals>
              </pluginExecutionFilter>
              <action>
                <ignore />
              </action>
            </pluginExecution>
          </pluginExecutions>
        </lifecycleMappingMetadata>
      </configuration>
    </plugin>
  </plugins>
</pluginManagement>

Conclusion


Voilà, nous savons désormais avec Maven, à partir d'un processeur d'annotation qui génère des classes et des ressources, compiler ce générateur, compiler nos tests qui utilisent ce générateur, et vérifier que les classes et les ressources générées sont accessibles et font ce qu'elles ont à faire.

Vous pouvez consulter le POM complet du projet Alternet Tools qui reprend tout ça.




Semantic Mismatch
ERR-07 : IllegalSortException
Can't apply TRIER on VOLET
-- See log for details



Aucun commentaire:

Enregistrer un commentaire