Skip to content

Commit

Permalink
Add support for launching OSGi Tests
Browse files Browse the repository at this point in the history
Currently PDE supports running a JUnit Plug-in Test, but this is tightly
coupled to PDE and Eclipse Applications. On the other hand PDE offers to
launch plain OSGi Frameworks but something similar for tests is missing.

This adds a new launch type "OSGi Test" that offers such type of tests
that are not tied to PDE/Eclipse but targeting generic OSGi
applications, this is very similar to what Tycho offers as
'tycho-surefire-plugin:bnd-test' and will also allow better interfacing
with the OSGi Test framework.
  • Loading branch information
laeubi committed Feb 21, 2025
1 parent b54cb7b commit 0c33ce1
Show file tree
Hide file tree
Showing 20 changed files with 731 additions and 30 deletions.
6 changes: 6 additions & 0 deletions features/org.eclipse.pde-feature/feature.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,19 @@
<import plugin="org.bndtools.templates.template"/>
<import plugin="biz.aQute.repository"/>
<import plugin="bndtools.jareditor"/>
<!-- TODO maybe better use a dedicated feature for easier inclusion in target platforms -->
<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"/>

<import plugin="biz.aQute.tester"/>
<import plugin="biz.aQute.tester.junit-platform"/>
<import plugin="biz.aQute.bnd.embedded-repo"/>

</requires>

<plugin
Expand Down
49 changes: 47 additions & 2 deletions ui/org.eclipse.pde.bnd.ui/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ One problem for such a reusable component is that it usually needs to get holds
To mitigate we use the [Eclipse Adapter Pattern](https://www.eclipse.org/articles/Article-Adapters/) as it is widely used in Eclipse, flexible and allows
the use of [OSGi services / Dependency Injection](https://eclipse.dev/eclipse/news/4.18/platform_isv.php#dialog-adapterfactory-as-service) already.

### The IProject adapter
### The `IProject` adapter

Components need to learn the project and workspace of a bndlib backed project, for this the very first step for an integration is to provide an adapter that can
transform an (Eclipse) `IProject` into a (bndlib) `Project` (from were the Workspace then can be derived), an example might look like this:

```
@Component
@AdapterTypes(adaptableClass = IProject.class, adapterNames = Project.class)
public class BndPluginAdapter implements IAdapterFactory {
public class BndProjectAdapter implements IAdapterFactory {
@Override
public <T> T getAdapter(Object adaptableObject, Class<T> adapterType) {
Expand All @@ -46,6 +46,51 @@ public class BndPluginAdapter implements IAdapterFactory {
}
```

### The `Bndrun` adapter

For some cases one would need to get hold of a `Bndrun` (e.g. for launching tests), to support this similar is needed as to create a template that then can further be customized.
For this use-case it is required to provide an adapter that can transform an (Eclipse) `IProject` into a (bndlib) `Bndrun` that is initialized with the runrequires contain the identity
of the project and the workspace be able to resolve such bundle.

```
@Component
@AdapterTypes(adaptableClass = IProject.class, adapterNames = Bndrun.class)
public class BndRunAdapter implements IAdapterFactory {
@Override
public <T> T getAdapter(Object adaptableObject, Class<T> adapterType) {
if (adaptableObject instanceof IProject eclipseProject) {
if (adapterType == Project.class) {
//... here we need to determine if the project is managed by our tooling, e.g. it is a PDE, Bndtool, Maven, ... backed project
if (/*... is relevant ... */) {
Workspace workspace = .... find the workspace according to your tooling implementation
Path base = workspace.getBase().toPath();
Path file = Files.createTempFile(base, project.getName(), ".bndrun");
file.toFile().deleteOnExit();
Files.createDirectories(file.getParent());
Properties properties = new Properties();
String bsn = ... derive the symbolic name ...
String version = ... derive the version name ...
properties.setProperty(Constants.RUNREQUIRES, String.format("bnd.identity; id=%s;version=%s", bsn, version));
//maybe more customizations....
Bndrun bndrun = Bndrun.createBndrun(workspace, file.toFile());
//make sure the file is deleted when the bndrun is closed...
bndrun.addClose(new AutoCloseable() {
@Override
public void close() throws Exception {
Files.delete(file);
}
});
}
}
}
return null;
}
}
```

## Available components

Beside some integration stuff (e.g. enable to [discover bndlib plugins](https://github.com/eclipse-pde/eclipse.pde/blob/master/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/internal/Auxiliary.java) inside an OSGi runtime)
Expand Down
4 changes: 3 additions & 1 deletion ui/org.eclipse.pde.core/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %name
Bundle-SymbolicName: org.eclipse.pde.core; singleton:=true
Bundle-Version: 3.20.100.qualifier
Bundle-Version: 3.21.0.qualifier
Bundle-Activator: org.eclipse.pde.internal.core.PDECore
Bundle-Vendor: %provider-name
Bundle-Localization: plugin
Expand Down Expand Up @@ -48,6 +48,7 @@ Export-Package:
org.eclipse.pde.ui,
org.eclipse.pde.api.tools.tests,
org.eclipse.pde.api.tools",
org.eclipse.pde.internal.core.osgitest;x-internal:=true,
org.eclipse.pde.internal.core.plugin;
x-friends:="org.eclipse.pde.ui,
org.eclipse.pde.ds.ui,
Expand Down Expand Up @@ -97,6 +98,7 @@ Import-Package: aQute.bnd.build;version="[4.4.0,5.0.0)",
aQute.bnd.service.progress;version="[1.3.0,2.0.0)",
aQute.bnd.version;version="[2.2.0,3.0.0)",
aQute.service.reporter;version="[1.2.0,2.0.0)",
biz.aQute.resolve;version="[9.1.0,10.0.0)",
org.bndtools.versioncontrol.ignores.manager.api;version="[1.0.0,2.0.0)",
org.eclipse.equinox.internal.p2.publisher.eclipse,
org.eclipse.equinox.p2.publisher,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,39 +27,39 @@ public interface IBuildEntry extends IWritable {
/**
* A property name for changes to the 'name' field.
*/
public static final String P_NAME = "name"; //$NON-NLS-1$
String P_NAME = "name"; //$NON-NLS-1$
/**
* The prefix for any key denoting the source folders that
* should be compiled into a JAR. The suffix will
* be the name of the JAR.
*/
public static final String JAR_PREFIX = "source."; //$NON-NLS-1$
String JAR_PREFIX = "source."; //$NON-NLS-1$
/**
* The prefix for any key denoting output folders for a particular
* JAR. The suffix will be the name of the JAR.
*/
public static final String OUTPUT_PREFIX = "output."; //$NON-NLS-1$
String OUTPUT_PREFIX = "output."; //$NON-NLS-1$
/**
* The name of the key that lists all the folders and files
* to be included in the binary build.
*/
public static final String BIN_INCLUDES = "bin.includes"; //$NON-NLS-1$
String BIN_INCLUDES = "bin.includes"; //$NON-NLS-1$
/**
* The name of the key that lists all the folders and files
* to be included in the source build.
*/
public static final String SRC_INCLUDES = "src.includes"; //$NON-NLS-1$
String SRC_INCLUDES = "src.includes"; //$NON-NLS-1$
/**
* The name of the key that declares extra library entries to be added
* to the class path at build time only..
*/
public static final String JARS_EXTRA_CLASSPATH = "jars.extra.classpath"; //$NON-NLS-1$
String JARS_EXTRA_CLASSPATH = "jars.extra.classpath"; //$NON-NLS-1$
/**
* The name of the key that declares additional plug-in dependencies to augment development classpath
*
* @since 3.2
*/
public static final String SECONDARY_DEPENDENCIES = "additional.bundles"; //$NON-NLS-1$
String SECONDARY_DEPENDENCIES = "additional.bundles"; //$NON-NLS-1$

/**
* Adds the token to the list of token for this entry.
Expand Down Expand Up @@ -89,6 +89,19 @@ public interface IBuildEntry extends IWritable {
*/
String[] getTokens();

/**
* Returns the first token for this entry
*
* @since 3.21
*/
default String getFirstToken() {
String[] tokens = getTokens();
if (tokens == null || tokens.length == 0) {
return null;
}
return tokens[0];
}

/**
* Returns true if the provided token exists in this entry.
* @param token the string token to look for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,14 @@ protected static Stream<IClasspathEntry> classpathEntriesForModelBundle(IPluginM
}
boolean isJarShape = new File(location).isFile();
IPluginLibrary[] libraries = model.getPluginBase().getLibraries();
if (isJarShape || libraries.length == 0) {
if (isJarShape) {
return Stream.of(getEntryForPath(IPath.fromOSString(location), extra));
}
if (libraries.length == 0) {
List<IClasspathEntry> entries = new ArrayList<>();
PDEClasspathContainer.addExternalPlugin(model, null, entries);
return entries.stream();
}
return Arrays.stream(libraries).filter(library -> !IPluginLibrary.RESOURCE.equals(library.getType()))
.map(library -> {
String name = library.getName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.pde.core.build.IBuild;
import org.eclipse.pde.core.build.IBuildEntry;
import org.eclipse.pde.core.plugin.IPluginBase;
import org.eclipse.pde.core.plugin.IPluginLibrary;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.core.build.ExternalBuildModel;
import org.osgi.resource.Resource;

public class PDEClasspathContainer {
Expand Down Expand Up @@ -78,7 +82,8 @@ public static IClasspathEntry[] getExternalEntries(IPluginModelBase model) {
}

protected static void addExternalPlugin(IPluginModelBase model, List<Rule> rules, List<IClasspathEntry> entries) {
boolean isJarShape = new File(model.getInstallLocation()).isFile();
File file = new File(model.getInstallLocation());
boolean isJarShape = file.isFile();
if (isJarShape) {
IPath srcPath = ClasspathUtilCore.getSourceAnnotation(model, ".", isJarShape); //$NON-NLS-1$
if (srcPath == null) {
Expand All @@ -93,14 +98,19 @@ protected static void addExternalPlugin(IPluginModelBase model, List<Rule> rules
addLibraryEntry(path, path, rules, getClasspathAttributes(model), entries);
}
} else {
IPluginLibrary[] libraries = model.getPluginBase().getLibraries();
IPluginBase pluginBase = model.getPluginBase();
IPluginLibrary[] libraries = pluginBase.getLibraries();
if (libraries.length == 0) {
// If there are no libraries, assume the root of the plug-in is the library '.'
IPath srcPath = ClasspathUtilCore.getSourceAnnotation(model, ".", isJarShape); //$NON-NLS-1$
if (srcPath == null) {
srcPath = IPath.fromOSString(model.getInstallLocation());
if (!addEntriesFromHostEclipse(model, rules, entries, file)) {
// If there are no libraries, assume the root of the plug-in
// is the library '.'
IPath srcPath = ClasspathUtilCore.getSourceAnnotation(model, ".", isJarShape); //$NON-NLS-1$
if (srcPath == null) {
srcPath = IPath.fromOSString(model.getInstallLocation());
}
addLibraryEntry(IPath.fromOSString(model.getInstallLocation()), srcPath, rules,
getClasspathAttributes(model), entries);
}
addLibraryEntry(IPath.fromOSString(model.getInstallLocation()), srcPath, rules, getClasspathAttributes(model), entries);
} else {
for (IPluginLibrary library : libraries) {
if (IPluginLibrary.RESOURCE.equals(library.getType())) {
Expand All @@ -125,6 +135,41 @@ protected static void addExternalPlugin(IPluginModelBase model, List<Rule> rules
}
}

protected static boolean addEntriesFromHostEclipse(IPluginModelBase model, List<Rule> rules,
List<IClasspathEntry> entries, File file) {
boolean hasBuildEntries = false;
// if build properties exits in folder than this is a local
// project from an Eclipse started from a workspace
if (new File(file, ICoreConstants.BUILD_FILENAME_DESCRIPTOR).isFile()) {
IBuild build = new ExternalBuildModel(model.getInstallLocation()).getBuild();
IBuildEntry[] buildEntries = build.getBuildEntries();
for (IBuildEntry entry : buildEntries) {
String name = entry.getName();
if (name.startsWith(IBuildEntry.OUTPUT_PREFIX)) {
String folder = entry.getFirstToken();
if (folder != null) {
File outputFolder = new File(file, folder);
if (outputFolder.isDirectory()) {
hasBuildEntries = true;
IBuildEntry sourceEntry = build.getEntry(IBuildEntry.JAR_PREFIX
+ name.substring(IBuildEntry.OUTPUT_PREFIX.length()));
IPath srcPath = null;
if (sourceEntry != null) {
String firstToken = sourceEntry.getFirstToken();
if (firstToken != null) {
srcPath = IPath.fromOSString(new File(file, firstToken).getAbsolutePath());
}
}
addLibraryEntry(IPath.fromOSString(outputFolder.getAbsolutePath()), srcPath, rules,
getClasspathAttributes(model), entries);
}
}
}
}
}
return hasBuildEntries;
}

protected static void addLibraryEntry(IPath path, IPath srcPath, List<Rule> rules, IClasspathAttribute[] attributes,
List<IClasspathEntry> entries) {
IClasspathEntry entry = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
package org.eclipse.pde.internal.core.bnd;

import java.io.File;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -33,6 +37,8 @@
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.plugin.IPluginModelBase;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.internal.core.annotations.OSGiAnnotationsClasspathContributor;
import org.eclipse.pde.internal.core.natures.BndProject;
Expand All @@ -44,6 +50,7 @@
import aQute.bnd.build.Workspace;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Processor;
import biz.aQute.resolve.Bndrun;

public class BndProjectManager {

Expand Down Expand Up @@ -96,8 +103,13 @@ static synchronized Workspace getWorkspace() throws Exception {
if (workspace == null) {
try (Processor run = new Processor()) {
run.setProperty(Constants.STANDALONE, TRUE);
IPath path = PDECore.getDefault().getStateLocation().append(Project.BNDCNF);
workspace = Workspace.createStandaloneWorkspace(run, path.toFile().toURI());
IPath path = PDECore.getDefault().getStateLocation().append(Constants.DEFAULT_BND_EXTENSION)
.append(Project.BNDCNF);
File file = path.toFile();
file.mkdirs();
workspace = Workspace.createStandaloneWorkspace(run, file.toURI());
workspace.setBase(file.getParentFile());
workspace.setBuildDir(file);
workspace.set("workspaceName", Messages.BndProjectManager_WorkspaceName); //$NON-NLS-1$
workspace.set("workspaceDescription", Messages.BndProjectManager_WorkspaceDescription); //$NON-NLS-1$
workspace.addBasicPlugin(TargetRepository.getTargetRepository());
Expand All @@ -111,8 +123,8 @@ static synchronized Workspace getWorkspace() throws Exception {
return workspace;
}

static void publishContainerEntries(List<IClasspathEntry> entries, Collection<Container> containers,
boolean isTest, Set<Project> mapped) {
static void publishContainerEntries(List<IClasspathEntry> entries, Collection<Container> containers, boolean isTest,
Set<Project> mapped) {
for (Container container : containers) {
TYPE type = container.getType();
if (type == TYPE.PROJECT) {
Expand Down Expand Up @@ -159,4 +171,37 @@ public static List<IClasspathEntry> getClasspathEntries(Project project, IWorksp
return entries;
}

public static Optional<Bndrun> createBndrun(IProject project) throws Exception {
if (BndProject.isBndProject(project) || PluginProject.isPluginProject(project)) {
IPluginModelBase model = PDECore.getDefault().getModelManager().findModel(project);
if (model == null) {
return Optional.empty();
}
Workspace workspace = getWorkspace();
Path base = workspace.getBase().toPath();
String name = project.getName();
Path file = Files.createTempFile(base, name, Constants.DEFAULT_BNDRUN_EXTENSION);
file.toFile().deleteOnExit();
Files.createDirectories(file.getParent());
Properties properties = new Properties();
BundleDescription description = model.getBundleDescription();
properties.setProperty(Constants.RUNREQUIRES, String.format("bnd.identity;id=%s;version=%s", //$NON-NLS-1$
description.getSymbolicName(), description.getVersion()));
try (OutputStream stream = Files.newOutputStream(file)) {
properties.store(stream,
String.format("Bndrun generated by PDE for project %s", name)); //$NON-NLS-1$
}
Bndrun bndrun = new PdeBndrun(workspace, file.toFile(), String.format("Bndrun for %s", name)); //$NON-NLS-1$
bndrun.addClose(new AutoCloseable() {

@Override
public void close() throws Exception {
Files.delete(file);
}
});
return Optional.of(bndrun);
}
return Optional.empty();
}

}
Loading

0 comments on commit 0c33ce1

Please sign in to comment.