Skip to content

Commit aef6d99

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 338779a commit aef6d99

File tree

10 files changed

+339
-24
lines changed

10 files changed

+339
-24
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<import plugin="org.bndtools.templates.template"/>
2929
<import plugin="biz.aQute.repository"/>
3030
<import plugin="bndtools.jareditor"/>
31+
<import feature="org.eclipse.pde.osgitest.dependencies.feature" version="1.0.0" match="greaterOrEqual"/>
3132
</requires>
3233

3334
<plugin
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<projectDescription>
3+
<name>org.eclipse.pde.osgitest.dependencies.feature</name>
4+
<comment></comment>
5+
<projects>
6+
</projects>
7+
<buildSpec>
8+
<buildCommand>
9+
<name>org.eclipse.pde.FeatureBuilder</name>
10+
<arguments>
11+
</arguments>
12+
</buildCommand>
13+
</buildSpec>
14+
<natures>
15+
<nature>org.eclipse.pde.FeatureNature</nature>
16+
</natures>
17+
</projectDescription>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
eclipse.preferences.version=1
2+
encoding/<project>=UTF-8
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
###############################################################################
2+
# Copyright (c) 2000, 2023 IBM Corporation 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+
# IBM Corporation - initial API and implementation
13+
###############################################################################
14+
bin.includes = feature.properties,\
15+
feature.xml
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
featureName=OSGi Test Dependencies Feature
15+
providerName=Eclipse.org
16+
description=Feature to consume OSGi Test (https://github.com/osgi/osgi-test/) dependencies in target platforms using an Eclipse Release.
17+
copyright=\
18+
Copyright (c) 2025 Christoph Läubrich and others.\n\
19+
\n\
20+
This program and the accompanying materials\n\
21+
are made available under the terms of the Eclipse Public License 2.0\n\
22+
which accompanies this distribution, and is available at\n\
23+
https://www.eclipse.org/legal/epl-2.0/\n\
24+
\n\
25+
SPDX-License-Identifier: EPL-2.0\n\
26+
\n\
27+
Contributors:\n\
28+
Christoph Läubrich - initial API and implementation\n
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<feature
3+
id="org.eclipse.pde.osgitest.dependencies.feature"
4+
label="%featureName"
5+
version="1.0.0.qualifier"
6+
license-feature="org.eclipse.license"
7+
license-feature-version="0.0.0">
8+
9+
<description url="https://github.com/eclipse-osgi-technology/osgi-test">
10+
%description
11+
</description>
12+
13+
<copyright>
14+
%copyright
15+
</copyright>
16+
17+
<license url="%licenseURL">
18+
%license
19+
</license>
20+
21+
<requires>
22+
<import plugin="org.osgi.test.common"/>
23+
<import plugin="org.osgi.test.junit5"/>
24+
<import plugin="org.osgi.test.junit5.cm"/>
25+
<import plugin="org.osgi.test.junit4"/>
26+
<import plugin="org.osgi.test.assertj.framework"/>
27+
<import plugin="org.osgi.test.assertj.log"/>
28+
<import plugin="org.osgi.test.assertj.promise"/>
29+
</requires>
30+
31+
</feature>

features/org.eclipse.pde.osgitest.dependencies.feature/forceQualifierUpdate.txt

Whitespace-only changes.

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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 87 additions & 23 deletions
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) {

0 commit comments

Comments
 (0)