Excluding packages from -buildpath or -testpath bundles

I have tried both directly in -buildpath or -testpath, like so:

-testpath: \
   my.dependency;packages=!problematic.package.*,*

And also decorated, like so:

-testpath: \
   my.dependency
-testpath+: \
    my.dependency;packages="!problematic.package.*,*"

and so:

-testpath: \
   my.dependency
-testpath+: \
    "my.dependency";packages="!problematic.package.*,*"

aaand so:

-testpath++: \
    my.dependency;packages=!problematic.package.*,*
-testpath++: \
    "my.dependency";packages=!problematic.package.*,*
-testpath++: \
    "my.dependency";packages="!problematic.package.*,*"

as a mix of these can be found in the bndtools sources.
Yet none of these ways to exclude a package from a build time dependency actually seem to hide the package from my bundle for the following scenario when starting a test with this configuration:

-buildpath: \
    bundle.containing.old.problematic.package
-testpath: \
    bundle.containing.newer.problematic.package
-testpath++: \
    "bundle.containing.old.problematic.package";packages="!problematic.package.*,*"

Is excluding packages from -buildpath dependencies during testing possible?

the packages are only used for compilation warnings when you compile against private code.

I am not sure what you’re trying to achieve? Your -testpath is the compile class path for test code and it is also used for plain JUnit launches. The packages attribute has zero effect on the launch classpath. For an Bnd OSGi Test launch, you get a lot more control with the -runpath, the system capabilities and packages in the bndrun file, and of course the magic you can do with bundles.

It would help if you define the problem you’re having?

Goal: see if Launchpad JUnit tests can be run out of one of our projects.

My test class:

package *REDACTED*;

import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.osgi.framework.BundleContext;

import aQute.launchpad.Launchpad;
import aQute.launchpad.LaunchpadBuilder;
import aQute.launchpad.Service;

class LaunchpadInClasspathModeTest {
	static final String EQUINOX_FRAMEWORK = "org.eclipse.osgi;version='[3.18,3.19)'";
	LaunchpadBuilder builder;
	
	@Service
	BundleContext context;
	
	@BeforeEach
	void before() {
		builder = new LaunchpadBuilder();
	}
	
	@Test
	void launchpadWorksInClasspathMode() throws Exception {
		try (Launchpad launchpad = builder.debug()
				.runfw(EQUINOX_FRAMEWORK)
				.create()) {
			assertTrue(launchpad.check());
		}
	}
}

Problem 1:
A -buildpath-included signed bundle -let’s call it platform.bundle1 exports its locally included org.slf4j:slf4j-api:2.0.13-packages all in version 1.7.36.
When aQute.launchpad.LaunchpadBuilder initializes its aQute.bnd.remoteworkspace.client.RemoteWorkspaceClientFactory, which in turn initializes its org.slf4j.Logger, the ClassLoader can’t find org.slf4j.spi.SLF4JServiceProvider:

// I lost the first line copy-pasting, so it isn't complete
java.lang.NoClassDefError
    at aQute.bnd.remoteworkspace.client.RemoteWorkspaceClientFactory.<clinit>(RemoteWorkspaceClientFactory.java:28)
    at aQute.launchpad.LaunchpadBuilder.<clinit>(LaunchpadBuilder.java:51)
    at *REDACTED*.LaunchpadInClasspathModeTest.before(LaunchpadInClasspathModeTest.java:22)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: java.lang.ClassNotFoundException: org.slf4j.spi.SLF4JServiceProvider
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
    ... 6 more

Once I include org.slf4j.simple in -testpath and resolve it via -runrequires for the -runbundles, via org.slf4j.simple’s Require-Bundle: slf4j.api, I have to add org.slf4j.api and once I add both in a higher version than 1.7.36, trying to run my test, the following Exception is thrown:

java.lang.ExceptionInInitializerError
    at aQute.bnd.remoteworkspace.client.RemoteWorkspaceClientFactory.<clinit>(RemoteWorkspaceClientFactory.java:28)
    at aQute.launchpad.LaunchpadBuilder.<clinit>(LaunchpadBuilder.java:51)
    at *REDACTED*.LaunchpadInClasspathModeTest.before(LaunchpadInClasspathModeTest.java:22)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: java.lang.SecurityException: class "org.slf4j.ILoggerFactory"'s signer information does not match signer information of other classes in the same package
    at java.base/java.lang.ClassLoader.checkCerts(ClassLoader.java:1163)
    at java.base/java.lang.ClassLoader.preDefineClass(ClassLoader.java:907)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1015)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862)
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
    at org.slf4j.LoggerFactory.<clinit>(LoggerFactory.java:95)
    ... 6 more

Now, after removing the signature keys and entries in platform.bundle1’s Manifest, my JUnit Console Output reads:

SLF4J: Class path contains multiple SLF4J providers.
SLF4J: Found provider [platform.bundle1.adapter.Slf4jProvider@21baa903]
SLF4J: Found provider [org.slf4j.simple.SimpleServiceProvider@607fbe09]
SLF4J: See https://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual provider is of type [platform.bundle1.adapter.Slf4jProvider@21baa903]

Now, for fixing the next two Exceptions after starting my test, I have to include org.osgi.util.tracker and org.osgi.resource in my -testpath, after that, Problem 2 starts.

Problem 2:
A -buildpath-included signed bundle -let’s call it platform.bundle2 exports org.osgi.service.repository;version="1.0.0" and I don’t know why, but all other classes typically provided by the system bundle get loaded from org.eclipse.osgi, yet this one suddenly gets provided by platform.bundle2 and the following Exception is thrown:

java.lang.NoSuchMethodError: 'void org.osgi.service.resolver.ResolveContext.onCancel(java.lang.Runnable)'
	at org.apache.felix.resolver.ResolverImpl$ResolveSession.createSession(ResolverImpl.java:99)
	at org.apache.felix.resolver.ResolverImpl.resolve(ResolverImpl.java:419)
	at org.apache.felix.resolver.ResolverImpl.resolve(ResolverImpl.java:374)
	at org.eclipse.osgi.container.ModuleResolver$ResolveProcess.resolveRevisions(ModuleResolver.java:1027)
	at org.eclipse.osgi.container.ModuleResolver$ResolveProcess.resolveRevisionsInBatch(ModuleResolver.java:981)
	at org.eclipse.osgi.container.ModuleResolver$ResolveProcess.resolve(ModuleResolver.java:898)
	at org.eclipse.osgi.container.ModuleResolver.resolveDelta(ModuleResolver.java:176)
	at org.eclipse.osgi.container.ModuleContainer.resolveAndApply(ModuleContainer.java:553)
	at org.eclipse.osgi.container.ModuleContainer.resolve(ModuleContainer.java:503)
	at org.eclipse.osgi.container.ModuleContainer.resolve(ModuleContainer.java:492)
	at org.eclipse.osgi.storage.Storage.checkSystemBundle(Storage.java:417)
	at org.eclipse.osgi.storage.Storage.createStorage(Storage.java:187)
	at org.eclipse.osgi.internal.framework.EquinoxContainer.<init>(EquinoxContainer.java:108)
	at org.eclipse.osgi.launch.Equinox.<init>(Equinox.java:53)
	at org.eclipse.osgi.launch.EquinoxFactory.newFramework(EquinoxFactory.java:35)
	at org.eclipse.osgi.launch.EquinoxFactory.newFramework(EquinoxFactory.java:30)
	at aQute.launchpad.LaunchpadBuilder.getFramework(LaunchpadBuilder.java:364)
	at aQute.launchpad.LaunchpadBuilder.create(LaunchpadBuilder.java:284)
	at aQute.launchpad.LaunchpadBuilder.create(LaunchpadBuilder.java:248)
	at *REDACTED*.LaunchpadInClasspathModeTest.launchpadWorksInClasspathMode(LaunchpadInClasspathModeTest.java:29)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

Like I said, don’t even attempt this until you got a clear separation with interfaces from your implementation code. It should only be used when you have a component and want to test it while using other bundles. However, it means the component should not see ANY implementation of the service it uses on the class path.

As said, it works like a charm when you have a clean build but without it it is hell.

SLF4J is a prefect example of class loading hacks. You can get it to work by putting the API and the implementation code on the -runpath. I.e. I often add slf4j.api and slf4j.simple to the -runpath.

You have to make absolutely sure that your APIs on the -testpath and -buildpath are identical to the APIs that the bundles depend on. There is no backward compatibility, versions must be identical.

I think you’re better off to focus on the normal OSGi testing. The fact that you have a random bundle exporting an OSGi API indicates that you just do not have a build on the modularity maturity level that launchpad requires. The non-modular code with lots of class loading tricks works more predictable with normal OSGi testing.

Just create a separate test project, write your junit code in a normal plain bundle, add the Test-Cases header and run the tests that way. A lot more predictable.

If you got your test ported that way, you could try to take one of your least coupled components and add component tests with launchpad. Its awfully cool but I think you want to run before you can walk here …

The rest of your answer other than this part I understand in the way that if I start a new project on a clean slate and can ensure proper API/impl separation and no bad dependencies (this might be the bummer, right?), I can sensibly start out with Launchpad JUnit tests for this bundle, however it might be too late for non-refactored existing codebases that first need said refactoring until Launchpad can even be initialized from a testsrc-included Test class. Is that a somewhat ok assessment?

But to make sure I understand the quoted sentence correctly in the sense of what kind of separation is necessary:
First some definitions as I’m not too firm with the OSGi spec and surely otherwise get something wrong

  • The bundle - com.example.whatever
  • The Service Package, exported, com.example.whatever.service, part of com.example.whatever
  • The Service, Interface, com.example.whatever.service.IWhateverService
  • The implementation package, private, com.example.whatever.service.impl, part of com.example.whatever
  • The component, concrete @Component annotated class, com.example.whatever.service.impl.WhateverService

Does your paragraph mean

  1. I must not have any bundle on com.example.whatever’s -buildpath that contains another @Component implementing com.example.whatever.service.IWhateverService?
    • or that
  2. com.example.whatever must be an API only bundle and com.example.whatever.service.impl sensibly would be the private main package of bundle com.example.whatever.service.impl?

The problem is that launchpad is balancing an extremely difficult trick. On one side you have the Java/Eclipse world that controls the classpath. In bnd this is -testpath + -buildpath. Unfortunately, Java always prefers classes from the classpath. So there is no way to override something on the classpath. This means that if any bundle uses a class X, and it happens to be on the classpath, it will get it from the classpath, even if it has a perfectly ok copy inside its bundle.

So your project might be a perfectly clean slate, but the bundles you depend on and load in launchpad might wreak havoc.

For my customers I generally start an API project that creates a JAR for each service API. The rule is then that no project is allowed to communicate with any other project except through its API. Very simple rule but I tend to have to threaten these with horrible torture and eternal damnation to get them to follow it. The sirens were toddlers in their luring capacity in comparison to this simple but evil class loading hack

The core idea is that the classpath ONLY contains API except for the component you’re testing. This requires a high discipline because if your component is based on some implementation code, it must be on the classpath and that likely messes up other bundles. It also requires that the bundles that are in the framework have substitutable exports so their classpath version can be the substitution. If you have different versions on the inside of the framework and the classpath: BOOOOOOM.

Launchpad like a Japanese knife. Its truly awesome but it can also really hurt your.

1 = yes
2 if your testing com.example.whatever,

  • its own service packages must be on the test path,
  • any used service package must be on the test path,
  • all bundles in the framework that depend on a service package on the test path must be
    • of the exact same version
    • must allow substitution of its exports (if it exports)

So the problem is that its not only your own bundle, also other bundles can mess you up. Unless you have a decent feeling of the class loading model of OSGi Launchpad will haunt you. So I pretty strongly advise you to use the Bnd OSGi test model.