Skip to content

Commit 24ca18d

Browse files
laeubiiloveeclipse
authored andcommitted
Limit access to junit platform engines
Currently the testruntime is exposed to multiple engines and the check for compatibility does not work well in the case of JUnit 5/6 on the classpath. This now - limit the dynamic import to a range of the currently only supported junit 5 - remove the falsely check for compatible engines - Use SPIClassloader to ensure compatible SPI are visible only - For JUnit 4 find the platform launcher as the SPI check
1 parent b6dd59b commit 24ca18d

File tree

6 files changed

+276
-94
lines changed

6 files changed

+276
-94
lines changed

ui/org.eclipse.pde.junit.runtime/META-INF/MANIFEST.MF

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ Export-Package: org.eclipse.pde.internal.junit.runtime;x-internal:=true
1212
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
1313
Bundle-ActivationPolicy: lazy
1414
Import-Package: org.eclipse.ui.testing;resolution:=optional
15-
DynamicImport-Package: org.junit.platform.engine
15+
DynamicImport-Package: org.junit.platform.engine;version="[1.14.0,2.0.0)"
1616
Automatic-Module-Name: org.eclipse.pde.junit.runtime
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Christoph Läubrich - initial API and implementation
12+
*
13+
*/
14+
package org.eclipse.pde.internal.junit.runtime;
15+
16+
import java.util.List;
17+
18+
import org.eclipse.core.runtime.Platform;
19+
import org.osgi.framework.Bundle;
20+
import org.osgi.framework.FrameworkUtil;
21+
import org.osgi.framework.wiring.BundleCapability;
22+
import org.osgi.framework.wiring.BundleWiring;
23+
24+
/**
25+
* Default implementation used with Java 1.8
26+
* TODO provide MR variant using stack walker, currently blocked by JDT bug ...
27+
*/
28+
public class Caller {
29+
private static final String JUNIT_PLATFORM_LAUNCHER = "org.junit.platform.launcher"; //$NON-NLS-1$
30+
private static final Bundle BUNDLE = FrameworkUtil.getBundle(Caller.class);
31+
private static final Bundle loaderBundle;
32+
33+
static {
34+
Bundle junit5RuntimeBundle = Platform.getBundle("org.eclipse.jdt.junit5.runtime"); //$NON-NLS-1$
35+
if (junit5RuntimeBundle == null) {
36+
Bundle junit4RuntimeBundle = Platform.getBundle("org.eclipse.jdt.junit4.runtime"); //$NON-NLS-1$
37+
loaderBundle = findJUnit5LauncherByRuntime(junit4RuntimeBundle);
38+
} else {
39+
loaderBundle = junit5RuntimeBundle;
40+
}
41+
}
42+
43+
protected static Bundle findJUnit5LauncherByRuntime(Bundle junit4RuntimeBundle) {
44+
if (junit4RuntimeBundle == null) {
45+
return BUNDLE;
46+
}
47+
for (Bundle bundle : BUNDLE.getBundleContext().getBundles()) {
48+
BundleWiring bundleWiring = bundle.adapt(BundleWiring.class);
49+
List<BundleCapability> capabilities = bundleWiring.getCapabilities(JUNIT_PLATFORM_LAUNCHER);
50+
if (!capabilities.isEmpty() && bundle.getVersion().getMajor() < 6) {
51+
return bundle;
52+
}
53+
}
54+
55+
return BUNDLE;
56+
}
57+
58+
static Bundle getBundle() {
59+
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
60+
for (StackTraceElement element : stackTraceElements) {
61+
try {
62+
String className = element.getClassName();
63+
Class<?> clz = loaderBundle.loadClass(className);
64+
Bundle bundle = FrameworkUtil.getBundle(clz);
65+
if (bundle == BUNDLE) {
66+
continue;
67+
}
68+
if (bundle != null) {
69+
return bundle;
70+
}
71+
} catch (ClassNotFoundException e) {
72+
continue;
73+
}
74+
}
75+
return null;
76+
}
77+
78+
}

ui/org.eclipse.pde.junit.runtime/src/org/eclipse/pde/internal/junit/runtime/MultiBundleClassLoader.java

Lines changed: 0 additions & 74 deletions
This file was deleted.

ui/org.eclipse.pde.junit.runtime/src/org/eclipse/pde/internal/junit/runtime/RemotePluginTestRunner.java

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
*******************************************************************************/
1515
package org.eclipse.pde.internal.junit.runtime;
1616

17-
import static java.util.stream.Collectors.toCollection;
18-
1917
import java.io.IOException;
2018
import java.net.URL;
2119
import java.util.ArrayList;
@@ -24,6 +22,7 @@
2422
import java.util.Enumeration;
2523
import java.util.List;
2624
import java.util.function.Predicate;
25+
import java.util.stream.Collectors;
2726

2827
import org.eclipse.core.runtime.Platform;
2928
import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;
@@ -143,35 +142,23 @@ private static ClassLoader createJUnit5PluginClassLoader(String testPluginName)
143142
if (junit5RuntimeBundle != null) {
144143
platformEngineBundles.add(junit5RuntimeBundle);
145144
}
146-
return new MultiBundleClassLoader(platformEngineBundles);
145+
return new SPIBundleClassLoader(platformEngineBundles);
147146
}
148147

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

154153
/**
155154
* Checks whether the bundle provides test engines.
156-
* Ensures that test engines that can be loaded from the bundle
157-
* are compatible with the TestEngine version in current scope.
158-
* Otherwise, the JUnit platform's call to the ServiceLoader for
159-
* retrieving available engines will fail.
160-
* Incompatibilities can happen, e.g., in Tycho builds, where
161-
* the org.eclipse.tycho.surefire.osgibooter bundle is found
162-
* that may provide a different JUnit platform version than the
163-
* one available via the Eclipse target platform.
164155
*/
165-
private static boolean providesCompatibleTestEngine(Bundle bundle) {
156+
private static boolean providesTestEngine(Bundle bundle) {
166157
try {
167158
BundleWiring bundleWiring = bundle.adapt(BundleWiring.class);
168159
String testEngineClass = "org.junit.platform.engine.TestEngine"; //$NON-NLS-1$
169160
Collection<String> engineProviders = bundleWiring.listResources("META-INF/services", testEngineClass, BundleWiring.LISTRESOURCES_LOCAL); //$NON-NLS-1$
170-
if (!engineProviders.isEmpty()) {
171-
Class<?> thisTestEngine = Class.forName(testEngineClass);
172-
Class<?> bundleTestEngine = bundle.loadClass(testEngineClass);
173-
return thisTestEngine == bundleTestEngine;
174-
}
161+
return !engineProviders.isEmpty();
175162
} catch (Exception e) {
176163
// skip this bundle
177164
}
@@ -204,7 +191,7 @@ public void init(String[] args) {
204191
// during initialization - see bug 520811
205192
ClassLoader currentTCCL = Thread.currentThread().getContextClassLoader();
206193
try {
207-
Thread.currentThread().setContextClassLoader(new MultiBundleClassLoader(findTestEngineBundles()));
194+
Thread.currentThread().setContextClassLoader(new SPIBundleClassLoader(findTestEngineBundles()));
208195
defaultInit(args);
209196
} finally {
210197
Thread.currentThread().setContextClassLoader(currentTCCL);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Christoph Läubrich - initial API and implementation
12+
*
13+
*/
14+
package org.eclipse.pde.internal.junit.runtime;
15+
16+
import java.io.IOException;
17+
import java.net.URL;
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.Enumeration;
22+
import java.util.Iterator;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.concurrent.ConcurrentHashMap;
26+
27+
import org.eclipse.core.runtime.FileLocator;
28+
import org.osgi.framework.Bundle;
29+
30+
/**
31+
* The classloader wraps the OSGi provided one but gives access for the JUnit
32+
* runer to any SPI declared services.
33+
*/
34+
class SPIBundleClassLoader extends ClassLoader {
35+
36+
private static final String META_INF_SERVICES = "META-INF/services/"; //$NON-NLS-1$
37+
private List<Bundle> bundles;
38+
private Map<String, List<SPIMapping>> mappings = new ConcurrentHashMap<>();
39+
40+
SPIBundleClassLoader(List<Bundle> bundles) {
41+
super(null);
42+
this.bundles = bundles;
43+
}
44+
45+
@Override
46+
protected Class<?> findClass(String name) throws ClassNotFoundException {
47+
Iterator<SPIMapping> spi = mappings.values().stream().flatMap(Collection::stream).filter(mapping -> mapping.hasService(name)).iterator();
48+
if (spi.hasNext()) {
49+
Bundle caller = Caller.getBundle();
50+
while (spi.hasNext()) {
51+
SPIMapping mapping = spi.next();
52+
if (mapping.isCompatible(caller)) {
53+
return mapping.loadImplementation(name);
54+
}
55+
}
56+
throw new ClassNotFoundException(name);
57+
}
58+
for (Bundle bundle : bundles) {
59+
try {
60+
Class<?> c = bundle.loadClass(name);
61+
if (c != null) {
62+
return c;
63+
}
64+
} catch (ClassNotFoundException e) {
65+
}
66+
}
67+
throw new ClassNotFoundException(name);
68+
}
69+
70+
@Override
71+
protected URL findResource(String name) {
72+
try {
73+
Enumeration<URL> resources = findResources(name);
74+
if (resources.hasMoreElements()) {
75+
return resources.nextElement();
76+
}
77+
} catch (IOException e) {
78+
}
79+
return null;
80+
}
81+
82+
@Override
83+
protected Enumeration<URL> findResources(String name) throws IOException {
84+
List<URL> result = new ArrayList<>();
85+
if (name.startsWith(META_INF_SERVICES)) {
86+
String serviceName = name.substring(META_INF_SERVICES.length());
87+
List<SPIMapping> spis = mappings.computeIfAbsent(name, spi -> {
88+
List<SPIMapping> list = new ArrayList<>();
89+
for (Bundle other : bundles) {
90+
URL entry = other.getEntry(name);
91+
if (entry != null) {
92+
try {
93+
list.add(new SPIMapping(other.loadClass(serviceName), other, entry));
94+
} catch (ClassNotFoundException e) {
95+
// should not happen
96+
}
97+
}
98+
}
99+
return list;
100+
});
101+
Bundle caller = Caller.getBundle();
102+
for (SPIMapping mapping : spis) {
103+
if (mapping.isCompatible(caller)) {
104+
result.add(mapping.getUrl());
105+
}
106+
}
107+
return Collections.enumeration(result);
108+
}
109+
for (Bundle bundle : bundles) {
110+
Enumeration<URL> resources = bundle.getResources(name);
111+
while (resources != null && resources.hasMoreElements()) {
112+
result.add(FileLocator.resolve(resources.nextElement()));
113+
}
114+
}
115+
return Collections.enumeration(result);
116+
}
117+
118+
@Override
119+
public String toString() {
120+
return "SPIBundleClassLoader for bundles " + bundles; //$NON-NLS-1$
121+
}
122+
123+
}

0 commit comments

Comments
 (0)