Skip to content

Commit

Permalink
Add OSGi test classpath support
Browse files Browse the repository at this point in the history
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 eclipse-pde#877
  • Loading branch information
laeubi committed Mar 11, 2025
1 parent 338779a commit aef6d99
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 24 deletions.
1 change: 1 addition & 0 deletions features/org.eclipse.pde-feature/feature.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<import plugin="org.bndtools.templates.template"/>
<import plugin="biz.aQute.repository"/>
<import plugin="bndtools.jareditor"/>
<import feature="org.eclipse.pde.osgitest.dependencies.feature" version="1.0.0" match="greaterOrEqual"/>
</requires>

<plugin
Expand Down
17 changes: 17 additions & 0 deletions features/org.eclipse.pde.osgitest.dependencies.feature/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.eclipse.pde.osgitest.dependencies.feature</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.pde.FeatureBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.pde.FeatureNature</nature>
</natures>
</projectDescription>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
###############################################################################
# Copyright (c) 2000, 2023 IBM Corporation and others.
#
# This program and the accompanying materials
# are made available under the terms of the Eclipse Public License 2.0
# which accompanies this distribution, and is available at
# https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# IBM Corporation - initial API and implementation
###############################################################################
bin.includes = feature.properties,\
feature.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
###############################################################################
# Copyright (c) 2025 Christoph Läubrich and others.
#
# This program and the accompanying materials
# are made available under the terms of the Eclipse Public License 2.0
# which accompanies this distribution, and is available at
# https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# Christoph Läubrich - initial API and implementation
###############################################################################
featureName=OSGi Test Dependencies Feature
providerName=Eclipse.org
description=Feature to consume OSGi Test (https://github.com/osgi/osgi-test/) dependencies in target platforms using an Eclipse Release.
copyright=\
Copyright (c) 2025 Christoph Läubrich and others.\n\
\n\
This program and the accompanying materials\n\
are made available under the terms of the Eclipse Public License 2.0\n\
which accompanies this distribution, and is available at\n\
https://www.eclipse.org/legal/epl-2.0/\n\
\n\
SPDX-License-Identifier: EPL-2.0\n\
\n\
Contributors:\n\
Christoph Läubrich - initial API and implementation\n
31 changes: 31 additions & 0 deletions features/org.eclipse.pde.osgitest.dependencies.feature/feature.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<feature
id="org.eclipse.pde.osgitest.dependencies.feature"
label="%featureName"
version="1.0.0.qualifier"
license-feature="org.eclipse.license"
license-feature-version="0.0.0">

<description url="https://github.com/eclipse-osgi-technology/osgi-test">
%description
</description>

<copyright>
%copyright
</copyright>

<license url="%licenseURL">
%license
</license>

<requires>
<import plugin="org.osgi.test.common"/>
<import plugin="org.osgi.test.junit5"/>
<import plugin="org.osgi.test.junit5.cm"/>
<import plugin="org.osgi.test.junit4"/>
<import plugin="org.osgi.test.assertj.framework"/>
<import plugin="org.osgi.test.assertj.log"/>
<import plugin="org.osgi.test.assertj.promise"/>
</requires>

</feature>
Empty file.
3 changes: 2 additions & 1 deletion ui/org.eclipse.pde.core/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,5 @@ Bundle-RequiredExecutionEnvironment: JavaSE-17
Bundle-ActivationPolicy: lazy
Automatic-Module-Name: org.eclipse.pde.core
Service-Component: OSGI-INF/org.eclipse.pde.internal.core.annotations.OSGiAnnotationsClasspathContributor.xml,
OSGI-INF/org.eclipse.pde.internal.core.bnd.PdeBndAdapter.xml
OSGI-INF/org.eclipse.pde.internal.core.bnd.PdeBndAdapter.xml,
OSGI-INF/org.eclipse.pde.internal.core.osgitest.OSGiTestClasspathContributor.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
Expand All @@ -33,6 +34,7 @@
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.pde.core.build.IBuild;
import org.eclipse.pde.core.build.IBuildModel;
import org.eclipse.pde.core.plugin.IFragment;
Expand All @@ -50,6 +52,10 @@
import org.eclipse.pde.internal.core.plugin.Fragment;
import org.eclipse.pde.internal.core.plugin.Plugin;
import org.eclipse.pde.internal.core.plugin.PluginBase;
import org.osgi.framework.Bundle;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.BundleWire;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.resource.Resource;

public class ClasspathUtilCore {
Expand Down Expand Up @@ -87,34 +93,92 @@ private static Collection<ClasspathLibrary> collectLibraryEntries(IPluginModelBa
}

public static Stream<IClasspathEntry> classpathEntriesForBundle(String id) {
return classpathEntriesForBundle(id, false, new IClasspathAttribute[0]);
}

public static Stream<IClasspathEntry> classpathEntriesForBundle(String id, boolean includeRequired,
IClasspathAttribute[] extra) {
// first look if we have something in the workspace...
IPluginModelBase model = PluginRegistry.findModel(id);
if (model != null && model.isEnabled()) {
IResource resource = model.getUnderlyingResource();
if (resource != null && PluginProject.isJavaProject(resource.getProject())) {
IJavaProject javaProject = JavaCore.create(resource.getProject());
return Stream.of(JavaCore.newProjectEntry(javaProject.getPath()));
}
String location = model.getInstallLocation();
if (location == null) {
return Stream.empty();
Stream<IClasspathEntry> modelBundleClasspath = classpathEntriesForModelBundle(model, extra);
if (includeRequired) {
return Stream.concat(modelBundleClasspath,
getRequiredByDescription(model.getBundleDescription(), extra));
}
boolean isJarShape = new File(location).isFile();
IPluginLibrary[] libraries = model.getPluginBase().getLibraries();
if (isJarShape || libraries.length == 0) {
return Stream.of(getEntryForPath(IPath.fromOSString(location)));
}
return Arrays.stream(libraries).filter(library -> !IPluginLibrary.RESOURCE.equals(library.getType()))
.map(library -> {
String name = library.getName();
String expandedName = ClasspathUtilCore.expandLibraryName(name);
return ClasspathUtilCore.getPath(model, expandedName, isJarShape);
}).filter(Objects::nonNull).map(ClasspathUtilCore::getEntryForPath);
return modelBundleClasspath;
}
// if not found in the models, try to use one from the running eclipse
return Optional.ofNullable(Platform.getBundle(id)).map(bundle -> bundle.adapt(File.class)).filter(File::exists)
Bundle runtimeBundle = Platform.getBundle(id);
if (runtimeBundle == null) {
return Stream.empty();
}
Stream<IClasspathEntry> bundleClasspath = classpathEntriesForRuntimeBundle(runtimeBundle, extra).stream();
if (includeRequired) {
return Stream.concat(bundleClasspath, getRequiredByWire(runtimeBundle, extra));
}
return bundleClasspath;
}

private static Stream<IClasspathEntry> getRequiredByDescription(BundleDescription description,
IClasspathAttribute[] extra) {
BundleWiring wiring = description.getWiring();
if (wiring == null) {
return Stream.empty();
}

List<BundleWire> wires = wiring.getRequiredWires(PackageNamespace.PACKAGE_NAMESPACE);
return wires.stream().map(wire -> {
return wire.getProvider();
}).distinct().flatMap(provider -> {
IPluginModelBase model = PluginRegistry.findModel(provider);
if (model != null && model.isEnabled()) {
return classpathEntriesForModelBundle(model, extra);
}
return Stream.empty();
});
}

protected static Stream<IClasspathEntry> classpathEntriesForModelBundle(IPluginModelBase model,
IClasspathAttribute[] extra) {
IResource resource = model.getUnderlyingResource();
if (resource != null && PluginProject.isJavaProject(resource.getProject())) {
IJavaProject javaProject = JavaCore.create(resource.getProject());
return Stream.of(JavaCore.newProjectEntry(javaProject.getPath()));
}
String location = model.getInstallLocation();
if (location == null) {
return Stream.empty();
}
boolean isJarShape = new File(location).isFile();
IPluginLibrary[] libraries = model.getPluginBase().getLibraries();
if (isJarShape || libraries.length == 0) {
return Stream.of(getEntryForPath(IPath.fromOSString(location), extra));
}
return Arrays.stream(libraries).filter(library -> !IPluginLibrary.RESOURCE.equals(library.getType()))
.map(library -> {
String name = library.getName();
String expandedName = ClasspathUtilCore.expandLibraryName(name);
return ClasspathUtilCore.getPath(model, expandedName, isJarShape);
}).filter(Objects::nonNull).map(entry -> getEntryForPath(entry, extra));
}

public static Stream<IClasspathEntry> getRequiredByWire(Bundle bundle, IClasspathAttribute[] extra) {
BundleWiring wiring = bundle.adapt(BundleWiring.class);
if (wiring == null) {
return Stream.empty();
}
List<BundleWire> wires = wiring.getRequiredWires(PackageNamespace.PACKAGE_NAMESPACE);
return wires.stream().map(wire -> wire.getProviderWiring().getBundle()).distinct()
.filter(b -> b.getBundleId() != 0)
.flatMap(b -> classpathEntriesForRuntimeBundle(b, extra).stream());
}

private static Optional<IClasspathEntry> classpathEntriesForRuntimeBundle(Bundle bundle,
IClasspathAttribute[] extra) {
return Optional.ofNullable(bundle.adapt(File.class)).filter(File::exists)
.map(File::toPath).map(Path::normalize).map(path -> IPath.fromOSString(path.toString()))
.map(ClasspathUtilCore::getEntryForPath).stream();
.map(entry -> getEntryForPath(entry, extra));
}

public static boolean isEntryForModel(IClasspathEntry entry, IPluginModelBase projectModel) {
Expand All @@ -127,8 +191,8 @@ public static boolean isEntryForModel(IClasspathEntry entry, IPluginModelBase pr
return false;
}

private static IClasspathEntry getEntryForPath(IPath path) {
return JavaCore.newLibraryEntry(path, path, IPath.ROOT, new IAccessRule[0], new IClasspathAttribute[0], false);
private static IClasspathEntry getEntryForPath(IPath path, IClasspathAttribute[] extra) {
return JavaCore.newLibraryEntry(path, path, IPath.ROOT, new IAccessRule[0], extra, false);
}

private static void addLibraryEntry(IPluginLibrary library, Collection<ClasspathLibrary> entries) {
Expand Down
Loading

0 comments on commit aef6d99

Please sign in to comment.