Thursday, April 23, 2009

Non intrusive OSGi Bundle Testing

For JBossOSGi I was looking for ways to test bundles that are deployed to a remote instance of the JBossOSGi Runtime. I wanted the solution to also work with an OSGi Framework that is bootstrapped from within a JUnit test case.

The basic problem is of course that you cannot access the artefacts that you deploy in a bundle directly from your test case, because they are loaded from different classloaders.



Most solutions to the problem somehow bring the test case into the OSGi Framework such that it can access the test bundle. They run the test and finally communicate the results back to the test framework. This approach is well documented in Chapter 11 of the Spring Dynamic Modules Reference Guide

Lets have a look at the specific JBossOSGi test requirements
  • OSGi Framework agnostic
  • non-intrusive to the test bundle
  • works for embedded and remote test scenarios
  • simple and fast to implement
To achieve that it seemed natural to leverage what OSGi provides natively in terms of reporting, which would be the Log Service from the OSGi compendium specification. The approach would work like this
  1. Test Case registers a LogListener with the LogReaderService
  2. Test bundle logs messages to the LogService
  3. Test Case verifies the order and content of the received log messages
The JBossOSGi SPI provides a special LogListener in form of the LogEntryCache to do the log entry caching, filtering and lookup.

But let's talk a little detour into the Log Service from the OSGi compendium specification.



LogService – The service interface that allows a bundle to log information, including a message, a level, an exception, a ServiceReference object, and a Bundle object.

LogEntry - An interface that allows access to a log entry in the log. It includes all the information that can be logged through the Log Service and a time stamp.

LogReaderService - A service interface that allows access to a list of recent LogEntry objects, and allows the registration of a LogListener object that receives LogEntry objects as they are created.

LogListener - The interface for the listener to LogEntry objects. Must be registered with the Log Reader Service.

First a test bundle needs to obtain an instance of the LogService. Here is a bad way of doing this
String sName = LogService.class.getName();
ServiceReference sRef = ctx.getServiceReference(sName);
LogService log = sysContext.getService(sRef);
The code above has two issues
  1. It assumes that there is LogService registered
  2. It does not detect when the LogService disappears
Here is a better way of obtaining the LogService
String sName = LogService.class.getName();
LogService log = new SystemLogService(context);
tracker = new ServiceTracker(context, sName, null)
{
public Object addingService(ServiceReference reference)
{
log = (LogService)super.addingService(reference);
return log;
}
public void removedService(ServiceReference reference, Object service)
{
super.removedService(reference, service);
log = new SystemLogService(context);
}
};
tracker.open();
The code above initializes the log instance with the SystemLogService that is part of the jboss-osgi-common.jar bundle. The tracker overrides the log instance when an actual LogService gets registered. The tracker restores the SystemLogService, in case the LogService disappears again.

This LogService tracking code is so common that the jboss-osgi-common.jar bundle already contains a tracker which does all of the above. Hence, the best way to obtaining the LogService with JBossOSGi ist
LogService log = new LogServiceTracker(context);
Now that the test bundle actually has an instance of the LogService it can start logging messages to that service. These messages are seen by the LogReaderService that the test case can register a LogListener with.

In the test case you would create a new LogEntryCache and associate one or more LogEntryFilter objects with it.
LogEntryCache logEntryCache = new LogEntryCache();
LogEntryFilter filter = new LogEntryFilter("example-log(.*)", 0, null);
logEntryCache.addFilter(filter);
The LogEntryCache is then added to the LogReaderService using a ServiceTracker similar to the one above. Because the filters are 'or' combined the cache stores log enties that pass at least one filter in the list. Each filter has three filter criteria
  1. A regular expression that matches a Bundle-SymbolicName
  2. A minimum LogLevel, whereas 0 indicates any LogLevel
  3. A regular expression that matches the log message
A test case can now verify the cached log messages like this
List entries = logEntryCache.getLog();
assertEquals("Number of entries", 1, entries.size());
assertEquals("[ServiceA] new Service", entries.get(0).getMessage());
In the remote test scenario it works essentially in the same way, only that a RemoteLogListener sends the LogEnties to a RemoteLogReaderService. The local test case registers it's LogEntryCache with the RemoteLogReaderService.


The actual communication is done with the JBoss Remoting socket transport. Both, the RemoteLogListener and the RemoteLogReaderService are part of the jboss-osgi-remotelog.jar bundle. That bundle needs to be installed with matching properties in the local and the remote OSGi Framework.
  • org.jboss.osgi.service.remote.log.sender - Enable the server side LogListener
  • org.jboss.osgi.service.remote.log.reader - Enable the client side LogReaderService
  • org.jboss.osgi.service.remote.log.host - The remote host that log messages are sent to
  • org.jboss.osgi.service.remote.log.port - The remote port that log messages are sent to
You may also want to have a look at Chapter 5.2 Remote Log Service of the JBossOSGi User Guide.

In my next post I'm going to look into the Blueprint Service (RFC-124) specification, which provides a rich component model for declaring components within a bundle and for instantiating, configuring, assembling and decorating such components - stay tuned.

May this be useful

No comments:

Post a Comment