Maven Native Chronicles: Running Automated Notes-based Tests
Feb 27, 2016, 5:02 PM
- Maven Native Chronicles, Part 1: Figuring Out nar-maven-plugin
- Maven Native Chronicles, Part 2: Setting Up a Windows Jenkins Node
- Maven Native Chronicles, Part 3: Improving Native Artifact Handling
- Maven Native Chronicles: Running Automated Notes-based Tests
This post isn't really in my ongoing Java thread, though it's related in that this is the sort of thing that may come up in fairly-advanced cases. This post will assume a functional knowledge of Maven, Tycho, and JUnit.
For Darwino, I ran into the need to run unit tests on Domino-adapter code during the Maven build process. Since the Domino project tree uses Tycho, this ended up differing slightly from standard Maven testing. Rather than using the src/test/java
directory in the same project to house the associated tests, Tycho prefers the very-OSGi-native method of having a separate project, but declaring it a "fragment" plugin attached to the primary one. In OSGi terms, a fragment is a special type of plugin that, when loaded by the runtime, gets glommed on to a specified host plugin and runs in its same classpath. In other cases, this may be used to provide platform-specific additions, add locale resources, or other uses.
So I created a new fragment project, which is structurally much like a normal plugin, but with an extra line in the MANIFEST.MF
:
Fragment-Host: com.example.some.parent.plugin
This line tips off the OSGi environment to its nature. In the pom.xml
, there are a number of important differences related both to how Tycho handles test fragments and the necessity of loading the Notes native libraries:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>some-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <artifactId>com.example.some.parent.plugin.test</artifactId> <packaging>eclipse-test-plugin</packaging> <build> <plugins> <!-- By default, Tycho doesn't include the other fragment plugins when running the test. So here, we manually include the appropriate features. --> <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>target-platform-configuration</artifactId> <version>${tycho-version}</version> <configuration> <dependency-resolution> <extraRequirements> <requirement> <type>eclipse-plugin</type> <id>com.ibm.notes.java.api.win32.linux</id> <versionRange>[9.0.1,9.0.2)</versionRange> </requirement> <requirement> <type>eclipse-feature</type> <id>com.example.some.native.feature</id> <versionRange>0.0.0</versionRange> </requirement> </extraRequirements> </dependency-resolution> </configuration> </plugin> <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>tycho-surefire-plugin</artifactId> <configuration> <testSuite>${project.artifactId}</testSuite> <testClass>com.example.some.parent.plugin.test.AllTests</testClass> </configuration> </plugin> </plugins> </build> </project>
The preamble is the same as usual for Maven, but the packaging is slightly different. Instead of eclipse-plugin
, this should be packaged as eclipse-test-plugin
. Tycho's packaging doesn't particularly care about whether or not it's a fragment, but it does care about its test nature.
Things get a little interesting in the target-platform-configuration
block. These two entries have similar purposes: to cause Tycho to load up other, native-artifact fragments required to run the tests. The first one showed up in the Java series: it contains Notes.jar
, but, because it is itself a fragment (and can't be directly depended upon by the test project), Tycho won't automatically load it unless directed to. The second one serves a similar purpose, but loads a feature instead. This feature contains references to a number of distinct platform-dependent native-artifact fragments, and specifying this dependency causes Tycho to consider each one without having to specifically enumerate them in the POM.
The final block is a little simpler, and it just tells Tycho where to start when it goes to run the fragment as a test suite. The AllTests
class is a test suite in the JUnit 4 convention, with @RunWith
and @Suite.SuiteClasses
annotations.
There's another catch to this, though: Notes has some specific demands on its environment, and in particular must be run with knowledge of a Notes program directory, a data directory, a notes.ini
, and an ID file (unless you're doing DIIOP (which you probably shouldn't)). The specifics of what the libraries expect in their runtime environment and how they should be loaded in their API calls vary a little from platform to platform, and I ended up with a pile of "just keep trying stuff until it works" code. The result, though, is that I have automated tests running on Windows, Linux, and OS X. First, there's the large platform-specific section of my root POM, which defines platform-activated profiles that set up environment variables:
<!-- These profiles add support for specific platforms for tests --> <profiles> <profile> <activation> <os> <family>Windows</family> </os> <property> <name>notes-program</name> </property> </activation> <build> <plugins> <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>tycho-surefire-plugin</artifactId> <version>${tycho-version}</version> <configuration> <skip>false</skip> <argLine>-Dfile.encoding=UTF-8 -Djava.library.path="${notes-program}"</argLine> <environmentVariables> <PATH>${notes-program}${path.separator}${env.PATH}</PATH> </environmentVariables> </configuration> </plugin> </plugins> </build> </profile> <profile> <id>mac</id> <activation> <os> <family>mac</family> </os> <property> <name>notes-program</name> </property> </activation> <build> <plugins> <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>tycho-surefire-plugin</artifactId> <configuration> <skip>false</skip> <argLine>-Dfile.encoding=UTF-8 -Djava.library.path="${notes-program}"</argLine> <environmentVariables> <PATH>${notes-program}${path.separator}${env.PATH}</PATH> <LD_LIBRARY_PATH>${notes-program}${path.separator}${env.LD_LIBRARY_PATH}</LD_LIBRARY_PATH> <DYLD_LIBRARY_PATH>${notes-program}${path.separator}${env.DYLD_LIBRARY_PATH}</DYLD_LIBRARY_PATH> <Notes_ExecDirectory>${notes-program}</Notes_ExecDirectory> </environmentVariables> </configuration> </plugin> </plugins> </build> </profile> <profile> <id>linux</id> <activation> <os> <family>unix</family> <name>linux</name> </os> <property> <name>notes-program</name> </property> </activation> <build> <plugins> <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>tycho-surefire-plugin</artifactId> <version>${tycho-version}</version> <configuration> <skip>false</skip> <argLine>-Dfile.encoding=UTF-8 -Djava.library.path="${notes-program}"</argLine> <environmentVariables> <!-- The res/C path entry is important for loading formula language properly --> <PATH>${notes-program}${path.separator}${notes-program}/res/C${path.separator}${notes-data}${path.separator}${env.PATH}</PATH> <LD_LIBRARY_PATH>${notes-program}${path.separator}${env.LD_LIBRARY_PATH}</LD_LIBRARY_PATH> <!-- Notes-standard environment variable to specify the program directory --> <Notes_ExecDirectory>${notes-program}</Notes_ExecDirectory> <Directory>${notes-data}</Directory> <!-- Linux generally requires that the notes.ini path be specified manually, since it's difficult to determine automatically --> <!-- This variable is a convention used in the test classes, not Notes-standard --> <NotesINI>${notes-ini}</NotesINI> </environmentVariables> </configuration> </plugin> </plugins> </build> </profile> </profiles>
Each block is kicked off both by a specific OS combination, using Maven's OS names (you can also target specific architectures within them), as well as the presence of a notes-program
property. This is a convention I've adopted to go alongside the notes-program
property that points to the XSP plugins; this one instead points to the root Notes or Domino install to use for execution.
Windows is the easiest since Notes still feels most at home on there. There, it's just a matter of adding the Notes program root to the Java library path and the environment's PATH
. From there, the Notes libraries automatically picked up the data directory and notes.ini
, presumably from the registry.
The Mac is mildly more complex: in addition to the two settings from Windows, I also ended up adding the program path to LD_LIBRARY_PATH
and DYLD_LIBRARY_PATH
. I'm not entirely sure both are needed, but hey, it works this way. In addition, I had to specify Notes_ExecDirectory
. After that, the tests found the location of the data dir and Notes Preferences
, presumably due to Mac OS conventions.
Linux needed the most hand-holding, which shouldn't be too surprising for those who have installed Domino on Linux - it doesn't seem to respect any platform conventions there. In addition to specifying the notes-program
property and using it in the same places as on the Mac, I also added two more properties to my Maven config: notes-data
, to point to the data directory, and notes-ini
, to point to notes.ini
. I used the notes-data
property to specify the Directory
environment variable that the Notes libraries look for, and then I also specified NotesINI
. That's not something that the Notes libs look for, but instead it's a way to shuttle the configuration to the Java code that actually executes the tests.
That leads to the final hurdle: initializing the Notes environment in the JUnit test classes. To do that, I specified a @BeforeClass
method that checks for the presence of the Notes_ExecDirectory
and NotesINI
environment variables. If they're present (i.e. it's Linux), it calls NotesInitExtended
with the value of Notes_ExecDirectory
as the first argument and =
plus the value of NotesINI
as the second. Afterwards, whether or not that was called, it calls NotesThread.sinitThread()
, and from then on NotesFactory.createSession()
will generate proper native sessions.
There's also an @AfterClass
method that is the mirror of that: it calls NotesThread.stermThread()
and then, on Linux, NotesTerm
.
So yeah, there are a lot of hoops to hop through! Hopefully, this post will be helpful for someone attempting to do the same thing I did, and it'll cut down on a lot of searching around and trying to piece together a working environment.