Skip to content

Commit b54cb7b

Browse files
committed
Add OSGi test classpath support
Similar to what we have for OSGi annotations, PDE should have OSGi Testing Support as it is a great library for testing OSGi applications. The most hindering thing in this regard is that it is rater complex to setup until one can make the first steps. This now adds a new classpath contributor that detects if a PDE project is already using JUNIT classpath container and then adds OSGi test dependencies automatically as test dependencies if they are part of the target platform or alternatively from the running platform. See #877
1 parent 975f72b commit b54cb7b

File tree

4 files changed

+252
-24
lines changed

4 files changed

+252
-24
lines changed

features/org.eclipse.pde-feature/feature.xml

+7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
<import plugin="org.bndtools.templates.template"/>
2929
<import plugin="biz.aQute.repository"/>
3030
<import plugin="bndtools.jareditor"/>
31+
<import plugin="org.osgi.test.common"/>
32+
<import plugin="org.osgi.test.junit5"/>
33+
<import plugin="org.osgi.test.junit5.cm"/>
34+
<import plugin="org.osgi.test.junit4"/>
35+
<import plugin="org.osgi.test.assertj.framework"/>
36+
<import plugin="org.osgi.test.assertj.log"/>
37+
<import plugin="org.osgi.test.assertj.promise"/>
3138
</requires>
3239

3340
<plugin

ui/org.eclipse.pde.core/META-INF/MANIFEST.MF

+2-1
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,5 @@ Bundle-RequiredExecutionEnvironment: JavaSE-17
134134
Bundle-ActivationPolicy: lazy
135135
Automatic-Module-Name: org.eclipse.pde.core
136136
Service-Component: OSGI-INF/org.eclipse.pde.internal.core.annotations.OSGiAnnotationsClasspathContributor.xml,
137-
OSGI-INF/org.eclipse.pde.internal.core.bnd.PdeBndAdapter.xml
137+
OSGI-INF/org.eclipse.pde.internal.core.bnd.PdeBndAdapter.xml,
138+
OSGI-INF/org.eclipse.pde.internal.core.osgitest.OSGiTestClasspathContributor.xml

ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ClasspathUtilCore.java

+87-23
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
2222
import java.util.Collection;
23+
import java.util.List;
2324
import java.util.Objects;
2425
import java.util.Optional;
2526
import java.util.stream.Stream;
@@ -33,6 +34,7 @@
3334
import org.eclipse.jdt.core.IClasspathEntry;
3435
import org.eclipse.jdt.core.IJavaProject;
3536
import org.eclipse.jdt.core.JavaCore;
37+
import org.eclipse.osgi.service.resolver.BundleDescription;
3638
import org.eclipse.pde.core.build.IBuild;
3739
import org.eclipse.pde.core.build.IBuildModel;
3840
import org.eclipse.pde.core.plugin.IFragment;
@@ -50,6 +52,10 @@
5052
import org.eclipse.pde.internal.core.plugin.Fragment;
5153
import org.eclipse.pde.internal.core.plugin.Plugin;
5254
import org.eclipse.pde.internal.core.plugin.PluginBase;
55+
import org.osgi.framework.Bundle;
56+
import org.osgi.framework.namespace.PackageNamespace;
57+
import org.osgi.framework.wiring.BundleWire;
58+
import org.osgi.framework.wiring.BundleWiring;
5359
import org.osgi.resource.Resource;
5460

5561
public class ClasspathUtilCore {
@@ -87,34 +93,92 @@ private static Collection<ClasspathLibrary> collectLibraryEntries(IPluginModelBa
8793
}
8894

8995
public static Stream<IClasspathEntry> classpathEntriesForBundle(String id) {
96+
return classpathEntriesForBundle(id, false, new IClasspathAttribute[0]);
97+
}
98+
99+
public static Stream<IClasspathEntry> classpathEntriesForBundle(String id, boolean includeRequired,
100+
IClasspathAttribute[] extra) {
90101
// first look if we have something in the workspace...
91102
IPluginModelBase model = PluginRegistry.findModel(id);
92103
if (model != null && model.isEnabled()) {
93-
IResource resource = model.getUnderlyingResource();
94-
if (resource != null && PluginProject.isJavaProject(resource.getProject())) {
95-
IJavaProject javaProject = JavaCore.create(resource.getProject());
96-
return Stream.of(JavaCore.newProjectEntry(javaProject.getPath()));
97-
}
98-
String location = model.getInstallLocation();
99-
if (location == null) {
100-
return Stream.empty();
104+
Stream<IClasspathEntry> modelBundleClasspath = classpathEntriesForModelBundle(model, extra);
105+
if (includeRequired) {
106+
return Stream.concat(modelBundleClasspath,
107+
getRequiredByDescription(model.getBundleDescription(), extra));
101108
}
102-
boolean isJarShape = new File(location).isFile();
103-
IPluginLibrary[] libraries = model.getPluginBase().getLibraries();
104-
if (isJarShape || libraries.length == 0) {
105-
return Stream.of(getEntryForPath(IPath.fromOSString(location)));
106-
}
107-
return Arrays.stream(libraries).filter(library -> !IPluginLibrary.RESOURCE.equals(library.getType()))
108-
.map(library -> {
109-
String name = library.getName();
110-
String expandedName = ClasspathUtilCore.expandLibraryName(name);
111-
return ClasspathUtilCore.getPath(model, expandedName, isJarShape);
112-
}).filter(Objects::nonNull).map(ClasspathUtilCore::getEntryForPath);
109+
return modelBundleClasspath;
113110
}
114111
// if not found in the models, try to use one from the running eclipse
115-
return Optional.ofNullable(Platform.getBundle(id)).map(bundle -> bundle.adapt(File.class)).filter(File::exists)
112+
Bundle runtimeBundle = Platform.getBundle(id);
113+
if (runtimeBundle == null) {
114+
return Stream.empty();
115+
}
116+
Stream<IClasspathEntry> bundleClasspath = classpathEntriesForRuntimeBundle(runtimeBundle, extra).stream();
117+
if (includeRequired) {
118+
return Stream.concat(bundleClasspath, getRequiredByWire(runtimeBundle, extra));
119+
}
120+
return bundleClasspath;
121+
}
122+
123+
private static Stream<IClasspathEntry> getRequiredByDescription(BundleDescription description,
124+
IClasspathAttribute[] extra) {
125+
BundleWiring wiring = description.getWiring();
126+
if (wiring == null) {
127+
return Stream.empty();
128+
}
129+
130+
List<BundleWire> wires = wiring.getRequiredWires(PackageNamespace.PACKAGE_NAMESPACE);
131+
return wires.stream().map(wire -> {
132+
return wire.getProvider();
133+
}).distinct().flatMap(provider -> {
134+
IPluginModelBase model = PluginRegistry.findModel(provider);
135+
if (model != null && model.isEnabled()) {
136+
return classpathEntriesForModelBundle(model, extra);
137+
}
138+
return Stream.empty();
139+
});
140+
}
141+
142+
protected static Stream<IClasspathEntry> classpathEntriesForModelBundle(IPluginModelBase model,
143+
IClasspathAttribute[] extra) {
144+
IResource resource = model.getUnderlyingResource();
145+
if (resource != null && PluginProject.isJavaProject(resource.getProject())) {
146+
IJavaProject javaProject = JavaCore.create(resource.getProject());
147+
return Stream.of(JavaCore.newProjectEntry(javaProject.getPath()));
148+
}
149+
String location = model.getInstallLocation();
150+
if (location == null) {
151+
return Stream.empty();
152+
}
153+
boolean isJarShape = new File(location).isFile();
154+
IPluginLibrary[] libraries = model.getPluginBase().getLibraries();
155+
if (isJarShape || libraries.length == 0) {
156+
return Stream.of(getEntryForPath(IPath.fromOSString(location), extra));
157+
}
158+
return Arrays.stream(libraries).filter(library -> !IPluginLibrary.RESOURCE.equals(library.getType()))
159+
.map(library -> {
160+
String name = library.getName();
161+
String expandedName = ClasspathUtilCore.expandLibraryName(name);
162+
return ClasspathUtilCore.getPath(model, expandedName, isJarShape);
163+
}).filter(Objects::nonNull).map(entry -> getEntryForPath(entry, extra));
164+
}
165+
166+
public static Stream<IClasspathEntry> getRequiredByWire(Bundle bundle, IClasspathAttribute[] extra) {
167+
BundleWiring wiring = bundle.adapt(BundleWiring.class);
168+
if (wiring == null) {
169+
return Stream.empty();
170+
}
171+
List<BundleWire> wires = wiring.getRequiredWires(PackageNamespace.PACKAGE_NAMESPACE);
172+
return wires.stream().map(wire -> wire.getProviderWiring().getBundle()).distinct()
173+
.filter(b -> b.getBundleId() != 0)
174+
.flatMap(b -> classpathEntriesForRuntimeBundle(b, extra).stream());
175+
}
176+
177+
private static Optional<IClasspathEntry> classpathEntriesForRuntimeBundle(Bundle bundle,
178+
IClasspathAttribute[] extra) {
179+
return Optional.ofNullable(bundle.adapt(File.class)).filter(File::exists)
116180
.map(File::toPath).map(Path::normalize).map(path -> IPath.fromOSString(path.toString()))
117-
.map(ClasspathUtilCore::getEntryForPath).stream();
181+
.map(entry -> getEntryForPath(entry, extra));
118182
}
119183

120184
public static boolean isEntryForModel(IClasspathEntry entry, IPluginModelBase projectModel) {
@@ -127,8 +191,8 @@ public static boolean isEntryForModel(IClasspathEntry entry, IPluginModelBase pr
127191
return false;
128192
}
129193

130-
private static IClasspathEntry getEntryForPath(IPath path) {
131-
return JavaCore.newLibraryEntry(path, path, IPath.ROOT, new IAccessRule[0], new IClasspathAttribute[0], false);
194+
private static IClasspathEntry getEntryForPath(IPath path, IClasspathAttribute[] extra) {
195+
return JavaCore.newLibraryEntry(path, path, IPath.ROOT, new IAccessRule[0], extra, false);
132196
}
133197

134198
private static void addLibraryEntry(IPluginLibrary library, Collection<ClasspathLibrary> entries) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Christoph Läubrich - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.pde.internal.core.osgitest;
15+
16+
import java.util.Collection;
17+
import java.util.Collections;
18+
import java.util.List;
19+
import java.util.concurrent.ConcurrentHashMap;
20+
import java.util.concurrent.ConcurrentMap;
21+
import java.util.function.Predicate;
22+
import java.util.stream.Stream;
23+
24+
import org.eclipse.core.resources.IProject;
25+
import org.eclipse.core.resources.IResource;
26+
import org.eclipse.core.runtime.IPath;
27+
import org.eclipse.core.runtime.Path;
28+
import org.eclipse.jdt.core.IClasspathAttribute;
29+
import org.eclipse.jdt.core.IClasspathEntry;
30+
import org.eclipse.jdt.core.IJavaProject;
31+
import org.eclipse.jdt.core.JavaCore;
32+
import org.eclipse.jdt.core.JavaModelException;
33+
import org.eclipse.osgi.service.resolver.BundleDelta;
34+
import org.eclipse.osgi.service.resolver.BundleDescription;
35+
import org.eclipse.osgi.service.resolver.State;
36+
import org.eclipse.osgi.service.resolver.StateDelta;
37+
import org.eclipse.pde.core.IClasspathContributor;
38+
import org.eclipse.pde.core.plugin.IPluginModelBase;
39+
import org.eclipse.pde.core.plugin.PluginRegistry;
40+
import org.eclipse.pde.internal.core.ClasspathUtilCore;
41+
import org.eclipse.pde.internal.core.IStateDeltaListener;
42+
import org.eclipse.pde.internal.core.PDECore;
43+
import org.osgi.resource.Resource;
44+
import org.osgi.service.component.annotations.Activate;
45+
import org.osgi.service.component.annotations.Component;
46+
import org.osgi.service.component.annotations.Deactivate;
47+
48+
/**
49+
* If the users chose to add JUNIT container to a PDE project and OSGi test is
50+
* in the target platform this contributor automatically adds the OSGi test
51+
* extensions to the classpath.
52+
*/
53+
@Component(service = IClasspathContributor.class)
54+
public class OSGiTestClasspathContributor implements IClasspathContributor, IStateDeltaListener {
55+
56+
private static final IPath PATH = new Path("org.eclipse.jdt.junit.JUNIT_CONTAINER"); //$NON-NLS-1$
57+
58+
private static final IPath JUNIT5_CONTAINER_PATH = PATH.append("5"); //$NON-NLS-1$
59+
private static final IPath JUNIT4_CONTAINER_PATH = PATH.append("4"); //$NON-NLS-1$
60+
61+
private static final int CHANGE_FLAGS = BundleDelta.ADDED | BundleDelta.REMOVED | BundleDelta.UPDATED;
62+
63+
private static final IClasspathAttribute TEST_ATTRIBUTE = JavaCore.newClasspathAttribute(IClasspathAttribute.TEST,
64+
"true"); //$NON-NLS-1$
65+
66+
private static final Collection<String> OSGI_TEST_BUNDLES = List.of("org.osgi.test.common", //$NON-NLS-1$
67+
"org.osgi.test.assertj.framework", "org.osgi.test.assertj.log", "org.osgi.test.assertj.promise"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
68+
69+
private static final Collection<String> OSGI_TEST_JUNIT5_BUNDLES = List.of("org.osgi.test.junit5", //$NON-NLS-1$
70+
"org.osgi.test.junit5.cm"); //$NON-NLS-1$
71+
72+
private static final Collection<String> OSGI_TEST_JUNIT4_BUNDLES = List.of("org.osgi.test.junit4"); //$NON-NLS-1$
73+
74+
private final ConcurrentMap<String, Collection<IClasspathEntry>> entryMap = new ConcurrentHashMap<>();
75+
76+
@Activate
77+
void registerListener() {
78+
// TODO we need to listen to classpath changes as well, eg. container
79+
// added/removed/changed/...
80+
PDECore.getDefault().getModelManager().addStateDeltaListener(this);
81+
}
82+
83+
@Deactivate
84+
void undregisterListener() {
85+
PDECore.getDefault().getModelManager().removeStateDeltaListener(this);
86+
}
87+
88+
/**
89+
* @return s stream of all osgi test bundles
90+
*/
91+
public static Stream<String> bundles(boolean junit5) {
92+
return Stream.concat(OSGI_TEST_BUNDLES.stream(),
93+
junit5 ? OSGI_TEST_JUNIT5_BUNDLES.stream() : OSGI_TEST_JUNIT4_BUNDLES.stream());
94+
}
95+
96+
@Override
97+
public List<IClasspathEntry> getInitialEntries(BundleDescription project) {
98+
IPluginModelBase projectModel = PluginRegistry.findModel((Resource) project);
99+
return junitBundles(projectModel)
100+
.map(bundleId -> entryMap.computeIfAbsent(bundleId,
101+
id -> ClasspathUtilCore
102+
.classpathEntriesForBundle(id, true, new IClasspathAttribute[] { TEST_ATTRIBUTE })
103+
.toList()))
104+
.flatMap(Collection::stream)
105+
.filter(Predicate.not(entry -> ClasspathUtilCore.isEntryForModel(entry, projectModel))).toList();
106+
}
107+
108+
protected Stream<String> junitBundles(IPluginModelBase projectModel) {
109+
IResource resource = projectModel.getUnderlyingResource();
110+
if (resource != null) {
111+
IProject eclipseProject = resource.getProject();
112+
IJavaProject javaProject = JavaCore.create(eclipseProject);
113+
try {
114+
IClasspathEntry[] classpath = javaProject.getRawClasspath();
115+
for (IClasspathEntry cp : classpath) {
116+
if (cp.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
117+
if (JUNIT5_CONTAINER_PATH.equals(cp.getPath())) {
118+
return bundles(true);
119+
}
120+
if (JUNIT4_CONTAINER_PATH.equals(cp.getPath())) {
121+
return bundles(false);
122+
}
123+
}
124+
}
125+
} catch (JavaModelException e) {
126+
// can't check, fall through and assume not enabled...
127+
}
128+
}
129+
return Stream.empty();
130+
}
131+
132+
@Override
133+
public List<IClasspathEntry> getEntriesForDependency(BundleDescription project, BundleDescription addedDependency) {
134+
return Collections.emptyList();
135+
}
136+
137+
@Override
138+
public void stateResolved(StateDelta delta) {
139+
if (delta == null) {
140+
stateChanged(null);
141+
} else {
142+
// just refresh the items in the map if they have any changes...
143+
for (BundleDelta bundleDelta : delta.getChanges(CHANGE_FLAGS, false)) {
144+
entryMap.remove(bundleDelta.getBundle().getSymbolicName());
145+
}
146+
}
147+
148+
}
149+
150+
@Override
151+
public void stateChanged(State newState) {
152+
// we need to refresh everything
153+
entryMap.clear();
154+
}
155+
156+
}

0 commit comments

Comments
 (0)