Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ui/org.eclipse.pde.junit.runtime/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: %pluginName
Bundle-SymbolicName: org.eclipse.pde.junit.runtime; singleton:=true
Bundle-Version: 3.8.200.qualifier
Bundle-Version: 3.8.300.qualifier
Bundle-Vendor: %providerName
Bundle-Localization: plugin
Require-Bundle: org.eclipse.jdt.junit.runtime;bundle-version="[3.4.0,4.0.0)",
Expand All @@ -12,5 +12,5 @@ Export-Package: org.eclipse.pde.internal.junit.runtime;x-internal:=true
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
Import-Package: org.eclipse.ui.testing;resolution:=optional
DynamicImport-Package: org.junit.platform.engine
DynamicImport-Package: org.junit.platform.engine;version="[1.14.0,2.0.0)"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to admit that I didn't knew that dynamically imported packages can have a version(range) too.
In general I wonder if we should better convert this to an optional package-import?
IIRC dynamic imports still create wires at runtime, but only on demand/dynamically. But as a test-runtime usually doesn't change I'm think if an ordinary (optional) import would be fine.
Rereading my comment from #1047 (comment), I think there was no greater meaning in it.
And we already have an optional requirement on org.eclipse.ui.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference between an optional and a dynamic import is that the optional one is resolved at the time the bundle is resolved, and the dynamic one is resolved at the time you try to access that package (+dynamic supports wildcards)

Automatic-Module-Name: org.eclipse.pde.junit.runtime
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*******************************************************************************
* 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
*
*/
package org.eclipse.pde.internal.junit.runtime;

import java.util.List;

import org.eclipse.core.runtime.Platform;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleWiring;

/**
* Default implementation used with Java 1.8
* TODO provide MR variant using stack walker, currently blocked by JDT bug ...
Comment on lines +25 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was about to suggest to use StackWalker. Until I was reminded that this bundle is still at Java-8 and until I read this comment.
Looking forward to see a MR bundle in action. I think this would be a good fit. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, sadly I was hit by this bug now but once this is merged it should work, and would be good if this feature is actually used in platform somewhere, but this is here really not high throughput method I just wanted to make it work first:

*/
public class Caller {
private static final String JUNIT_PLATFORM_LAUNCHER = "org.junit.platform.launcher"; //$NON-NLS-1$
private static final Bundle BUNDLE = FrameworkUtil.getBundle(Caller.class);
private static final Bundle loaderBundle;

static {
Bundle junit5RuntimeBundle = Platform.getBundle("org.eclipse.jdt.junit5.runtime"); //$NON-NLS-1$
if (junit5RuntimeBundle == null) {
Bundle junit4RuntimeBundle = Platform.getBundle("org.eclipse.jdt.junit4.runtime"); //$NON-NLS-1$
loaderBundle = findJUnit5LauncherByRuntime(junit4RuntimeBundle);
} else {
loaderBundle = junit5RuntimeBundle;
}
}

protected static Bundle findJUnit5LauncherByRuntime(Bundle junit4RuntimeBundle) {
if (junit4RuntimeBundle == null) {
return BUNDLE;
}
for (Bundle bundle : BUNDLE.getBundleContext().getBundles()) {
BundleWiring bundleWiring = bundle.adapt(BundleWiring.class);
List<BundleCapability> capabilities = bundleWiring.getCapabilities(JUNIT_PLATFORM_LAUNCHER);
if (!capabilities.isEmpty() && bundle.getVersion().getMajor() < 6) {
return bundle;
}
}

return BUNDLE;
}

static Bundle getBundle() {
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
for (StackTraceElement element : stackTraceElements) {
try {
String className = element.getClassName();
Class<?> clz = loaderBundle.loadClass(className);
Bundle bundle = FrameworkUtil.getBundle(clz);
if (bundle == BUNDLE) {
continue;
}
if (bundle != null) {
return bundle;
}
} catch (ClassNotFoundException e) {
continue;
}
}
return null;
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
*******************************************************************************/
package org.eclipse.pde.internal.junit.runtime;

import static java.util.stream.Collectors.toCollection;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
Expand All @@ -24,6 +22,7 @@
import java.util.Enumeration;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;
Expand Down Expand Up @@ -143,35 +142,23 @@ private static ClassLoader createJUnit5PluginClassLoader(String testPluginName)
if (junit5RuntimeBundle != null) {
platformEngineBundles.add(junit5RuntimeBundle);
}
return new MultiBundleClassLoader(platformEngineBundles);
return new SPIBundleClassLoader(platformEngineBundles);
}

private static List<Bundle> findTestEngineBundles() {
BundleContext bundleContext = FrameworkUtil.getBundle(RemotePluginTestRunner.class).getBundleContext();
return Arrays.stream(bundleContext.getBundles()).filter(RemotePluginTestRunner::providesCompatibleTestEngine).collect(toCollection(ArrayList::new));
return Arrays.stream(bundleContext.getBundles()).filter(RemotePluginTestRunner::providesTestEngine).collect(Collectors.toCollection(ArrayList::new));
}

/**
* Checks whether the bundle provides test engines.
* Ensures that test engines that can be loaded from the bundle
* are compatible with the TestEngine version in current scope.
* Otherwise, the JUnit platform's call to the ServiceLoader for
* retrieving available engines will fail.
* Incompatibilities can happen, e.g., in Tycho builds, where
* the org.eclipse.tycho.surefire.osgibooter bundle is found
* that may provide a different JUnit platform version than the
* one available via the Eclipse target platform.
*/
private static boolean providesCompatibleTestEngine(Bundle bundle) {
private static boolean providesTestEngine(Bundle bundle) {
try {
BundleWiring bundleWiring = bundle.adapt(BundleWiring.class);
String testEngineClass = "org.junit.platform.engine.TestEngine"; //$NON-NLS-1$
Collection<String> engineProviders = bundleWiring.listResources("META-INF/services", testEngineClass, BundleWiring.LISTRESOURCES_LOCAL); //$NON-NLS-1$
if (!engineProviders.isEmpty()) {
Class<?> thisTestEngine = Class.forName(testEngineClass);
Class<?> bundleTestEngine = bundle.loadClass(testEngineClass);
return thisTestEngine == bundleTestEngine;
}
return !engineProviders.isEmpty();
} catch (Exception e) {
// skip this bundle
}
Expand Down Expand Up @@ -204,7 +191,7 @@ public void init(String[] args) {
// during initialization - see bug 520811
ClassLoader currentTCCL = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(new MultiBundleClassLoader(findTestEngineBundles()));
Thread.currentThread().setContextClassLoader(new SPIBundleClassLoader(findTestEngineBundles()));
defaultInit(args);
} finally {
Thread.currentThread().setContextClassLoader(currentTCCL);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*******************************************************************************
* 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
*
*/
package org.eclipse.pde.internal.junit.runtime;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.core.runtime.FileLocator;
import org.osgi.framework.Bundle;

/**
* The classloader wraps the OSGi provided one but gives access for the JUnit
* runer to any SPI declared services.
*/
class SPIBundleClassLoader extends ClassLoader {

private static final String META_INF_SERVICES = "META-INF/services/"; //$NON-NLS-1$
private List<Bundle> bundles;
private Map<String, List<SPIMapping>> mappings = new ConcurrentHashMap<>();

SPIBundleClassLoader(List<Bundle> bundles) {
super(null);
this.bundles = bundles;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Iterator<SPIMapping> spi = mappings.values().stream().flatMap(Collection::stream).filter(mapping -> mapping.hasService(name)).iterator();
if (spi.hasNext()) {
Bundle caller = Caller.getBundle();
while (spi.hasNext()) {
SPIMapping mapping = spi.next();
if (mapping.isCompatible(caller)) {
return mapping.loadImplementation(name);
}
}
throw new ClassNotFoundException(name);
}
for (Bundle bundle : bundles) {
try {
Class<?> c = bundle.loadClass(name);
if (c != null) {
return c;
}
} catch (ClassNotFoundException e) {
}
}
throw new ClassNotFoundException(name);
}

@Override
protected URL findResource(String name) {
try {
Enumeration<URL> resources = findResources(name);
if (resources.hasMoreElements()) {
return resources.nextElement();
}
} catch (IOException e) {
}
return null;
}

@Override
protected Enumeration<URL> findResources(String name) throws IOException {
List<URL> result = new ArrayList<>();
if (name.startsWith(META_INF_SERVICES)) {
String serviceName = name.substring(META_INF_SERVICES.length());
List<SPIMapping> spis = mappings.computeIfAbsent(name, spi -> {
List<SPIMapping> list = new ArrayList<>();
for (Bundle other : bundles) {
URL entry = other.getEntry(name);
if (entry != null) {
try {
list.add(new SPIMapping(other.loadClass(serviceName), other, entry));
} catch (ClassNotFoundException e) {
// should not happen
}
}
}
return list;
});
Bundle caller = Caller.getBundle();
for (SPIMapping mapping : spis) {
if (mapping.isCompatible(caller)) {
result.add(mapping.getUrl());
}
}
return Collections.enumeration(result);
}
for (Bundle bundle : bundles) {
Enumeration<URL> resources = bundle.getResources(name);
while (resources != null && resources.hasMoreElements()) {
result.add(FileLocator.resolve(resources.nextElement()));
}
}
return Collections.enumeration(result);
}

@Override
public String toString() {
return "SPIBundleClassLoader for bundles " + bundles; //$NON-NLS-1$
}

}
Loading
Loading