Skip to content

Commit fcecab2

Browse files
authored
Merge pull request #7000 from chrisrueger/copilot/fix-no-namespaced-hierarchical-store
Fix Junit Platform 1.13+ Support (up to JUnit Jupiter and Platform to 5.14.2/1.14.2)
2 parents 0375222 + 0ca1bac commit fcecab2

File tree

11 files changed

+193
-24
lines changed

11 files changed

+193
-24
lines changed

biz.aQute.tester.junit-platform/bnd.bnd

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,11 @@ Bundle-Description: \
1212
# The dependency on aQute packages is only for the
1313
# launcher side. When launched, those dependencies
1414
# are not necessary
15-
# Note about the unusually restricted version range for org.junit.platform - refer
16-
# GitHub issue #6651. The original import package directive is here and can be restored
17-
# if/when #6651 is fixed:
18-
# org.junit.platform.*;version="${range;[==,+);${junit.platform.tester.version}}",\
1915
Import-Package: \
2016
aQute.*;resolution:=optional,\
2117
junit.*;version="${range;[==,5);${junit3.version}}";resolution:=optional,\
2218
org.apache.felix.service.command;resolution:=optional,\
23-
org.junit.platform.*;version="[${junit.platform.tester.version},1.13)",\
19+
org.junit.platform.*;version="${range;[==,+);${junit.platform.tester.version}}",\
2420
org.junit.*;version="${range;[==,+);${junit4.tester.version}}";resolution:=optional,\
2521
*
2622

biz.aQute.tester.junit-platform/src/aQute/tester/bundle/engine/BundleDescriptor.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import java.util.HashMap;
44
import java.util.Map;
55

6-
import org.junit.platform.engine.ConfigurationParameters;
7-
import org.junit.platform.engine.EngineExecutionListener;
86
import org.junit.platform.engine.ExecutionRequest;
97
import org.junit.platform.engine.TestDescriptor;
108
import org.junit.platform.engine.TestEngine;
@@ -42,12 +40,10 @@ public void addChild(TestDescriptor descriptor, TestEngine engine) {
4240
addChild(descriptor);
4341
}
4442

45-
public void executeChild(TestDescriptor descriptor, EngineExecutionListener listener,
46-
ConfigurationParameters params) {
43+
public void executeChild(TestDescriptor descriptor, ExecutionRequest parentRequest) {
4744
TestEngine engine = engineMap.get(descriptor);
48-
ExecutionRequest er = new ExecutionRequest(descriptor, listener, params);
49-
engine.execute(er);
50-
45+
ExecutionRequest childRequest = ExecutionRequestFactory.createChildRequest(descriptor, parentRequest);
46+
engine.execute(childRequest);
5147
}
5248

5349
@Override

biz.aQute.tester.junit-platform/src/aQute/tester/bundle/engine/BundleEngine.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public void execute(ExecutionRequest request) {
9797
.getDisplayName());
9898
childDescriptors.forEach(childDescriptor -> listener.executionSkipped(childDescriptor, reason));
9999
} else {
100-
childDescriptors.forEach(childDescriptor -> executeBundle(childDescriptor, listener, params));
100+
childDescriptors.forEach(childDescriptor -> executeBundle(childDescriptor, listener, params, request));
101101
}
102102
listener.executionFinished(root, TestExecutionResult.successful());
103103
} catch (Throwable t) {
@@ -108,7 +108,7 @@ public void execute(ExecutionRequest request) {
108108
}
109109

110110
private static void executeBundle(BundleDescriptor descriptor, EngineExecutionListener listener,
111-
ConfigurationParameters params) {
111+
ConfigurationParameters params, ExecutionRequest parentRequest) {
112112
listener.executionStarted(descriptor);
113113
TestExecutionResult result;
114114
if (descriptor.getException() == null) {
@@ -117,7 +117,7 @@ private static void executeBundle(BundleDescriptor descriptor, EngineExecutionLi
117117
.stream()
118118
.filter(childDescriptor -> !(childDescriptor instanceof BundleDescriptor
119119
|| childDescriptor instanceof StaticFailureDescriptor))
120-
.forEach(childDescriptor -> descriptor.executeChild(childDescriptor, listener, params));
120+
.forEach(childDescriptor -> descriptor.executeChild(childDescriptor, parentRequest));
121121
result = TestExecutionResult.successful();
122122
} catch (TestAbortedException abort) {
123123
result = TestExecutionResult.aborted(abort);
@@ -130,7 +130,7 @@ private static void executeBundle(BundleDescriptor descriptor, EngineExecutionLi
130130
.stream()
131131
.filter(BundleDescriptor.class::isInstance)
132132
.map(BundleDescriptor.class::cast)
133-
.forEach(childDescriptor -> executeBundle(childDescriptor, listener, params));
133+
.forEach(childDescriptor -> executeBundle(childDescriptor, listener, params, parentRequest));
134134
descriptor.getChildren()
135135
.stream()
136136
.filter(StaticFailureDescriptor.class::isInstance)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package aQute.tester.bundle.engine;
2+
3+
import java.lang.reflect.Method;
4+
import java.lang.reflect.Modifier;
5+
6+
import org.junit.platform.engine.ConfigurationParameters;
7+
import org.junit.platform.engine.EngineExecutionListener;
8+
import org.junit.platform.engine.ExecutionRequest;
9+
import org.junit.platform.engine.TestDescriptor;
10+
11+
/**
12+
* Factory for creating ExecutionRequest instances that are compatible with both
13+
* JUnit Platform < 1.13 and >= 1.13. In JUnit Platform 1.13+, the internal
14+
* NamespacedHierarchicalStore must be properly propagated to child execution
15+
* requests. This class use reflection magic, but unfortunately this is
16+
* necessary to support wide range of runtime JUnit version where we need to use
17+
* internal API which drastically changes between versions.
18+
*/
19+
public class ExecutionRequestFactory {
20+
21+
private static final boolean USE_REFLECTION;
22+
/**
23+
* Reference to the getStore() method (new in 1.13+)
24+
*/
25+
private static final Method GET_STORE_METHOD;
26+
27+
/**
28+
* reference to static create() methods with 3-arguments
29+
* org.junit.platform.engine.ExecutionRequest.create(TestDescriptor,
30+
* EngineExecutionListener, ConfigurationParameters)
31+
*/
32+
static final Method CREATE_3;
33+
34+
/**
35+
* reference to static create() methods with 5-arguments
36+
* org.junit.platform.engine.ExecutionRequest.create(TestDescriptor,
37+
* EngineExecutionListener, ConfigurationParameters)
38+
*/
39+
static final Method CREATE_5;
40+
41+
42+
static {
43+
boolean useReflection = false;
44+
Method getStoreMethod = null;
45+
Method c3 = null, c5 = null;
46+
47+
try {
48+
49+
// Look for 5-parameter create() method (1.13+): descriptor,
50+
// listener, params, outputProvider, store
51+
// or 4-parameter constructor (earlier versions): descriptor,
52+
// listener, params, store
53+
for (Method m : ExecutionRequest.class.getDeclaredMethods()) {
54+
if (!m.getName().equals("create")
55+
|| !Modifier.isStatic(m.getModifiers())
56+
|| m.getReturnType() != ExecutionRequest.class) {
57+
continue;
58+
}
59+
60+
Class<?>[] p = m.getParameterTypes();
61+
62+
if (p.length == 3
63+
&& p[0] == TestDescriptor.class
64+
&& p[1] == EngineExecutionListener.class
65+
&& p[2] == ConfigurationParameters.class) {
66+
// this is the deprecated 3-arg
67+
c3 = m;
68+
}
69+
else if (p.length == 5
70+
&& p[0] == TestDescriptor.class
71+
&& p[1] == EngineExecutionListener.class
72+
&& p[2] == ConfigurationParameters.class) {
73+
// 5-arg version
74+
c5 = m;
75+
}
76+
}
77+
78+
79+
// Try to access the internal Store from ExecutionRequest
80+
// This accesses JUnit Platform's internal API which is necessary for
81+
// 1.13+ compatibility.
82+
// The Store class and getStore() method are internal implementation
83+
// details
84+
// that may change in future versions, but are required to properly
85+
// propagate
86+
// execution context to nested test engines.
87+
getStoreMethod = ExecutionRequest.class.getDeclaredMethod("getStore");
88+
getStoreMethod.setAccessible(true);
89+
90+
// Get the Store class from the method's return type
91+
Class<?> storeClass = getStoreMethod.getReturnType();
92+
93+
// Only use reflection if we found both the getter one of the create methods
94+
useReflection = (getStoreMethod != null && (c3 != null || c5 != null));
95+
} catch (Exception e) {
96+
// Reflection not available or not needed, fall back to public
97+
// constructor
98+
}
99+
100+
USE_REFLECTION = useReflection;
101+
GET_STORE_METHOD = getStoreMethod;
102+
CREATE_3 = c3;
103+
CREATE_5 = c5;
104+
105+
}
106+
107+
/**
108+
* Create a new ExecutionRequest for a child engine, properly propagating
109+
* the execution context from the parent request.
110+
*
111+
* @param descriptor The root test descriptor for the child engine
112+
* @param parentRequest The parent execution request to derive context from
113+
* @return A new ExecutionRequest for the child engine
114+
*/
115+
static ExecutionRequest createChildRequest(TestDescriptor descriptor, ExecutionRequest parentRequest) {
116+
EngineExecutionListener listener = parentRequest.getEngineExecutionListener();
117+
ConfigurationParameters params = parentRequest.getConfigurationParameters();
118+
119+
if (USE_REFLECTION) {
120+
try {
121+
// Get the Store from the parent request
122+
Object store = GET_STORE_METHOD.invoke(parentRequest);
123+
124+
// Create a new ExecutionRequest with the Store
125+
126+
if (CREATE_5 != null) {
127+
// JUnit Platform 1.13+: need to pass
128+
// OutputDirectoryProvider as 4th param
129+
// Get the OutputDirectoryProvider from parent request
130+
// TODO from 1.14+ getOutputDirectoryCreator() replaces
131+
// "getOutputDirectoryProvider". So we may have to adopt
132+
// that in the future
133+
Method getOutputProvider = ExecutionRequest.class.getDeclaredMethod("getOutputDirectoryProvider");
134+
getOutputProvider.setAccessible(true);
135+
Object outputProvider = getOutputProvider.invoke(parentRequest);
136+
137+
return (ExecutionRequest) CREATE_5.invoke(parentRequest, descriptor, listener, params,
138+
outputProvider,
139+
store);
140+
}
141+
else {
142+
// Earlier versions: 3-param static create() method
143+
return (ExecutionRequest) CREATE_3.invoke(parentRequest, descriptor, listener, params);
144+
}
145+
} catch (Exception e) {
146+
// Fall back to public constructor if reflection fails
147+
// Using System.err because this is a test framework component that
148+
// needs
149+
// to report issues even when no logging framework is available
150+
System.err.println(
151+
"Warning: BundleEngine failed to propagate execution context for " + descriptor.getDisplayName()
152+
+ " using reflection, falling back to public constructor. " + "JUnit Platform extensions may not work correctly. Cause: "
153+
+ e.getClass()
154+
.getName()
155+
+ ": " + e.getMessage());
156+
e.printStackTrace();
157+
}
158+
}
159+
160+
// Use the public constructor (works for JUnit Platform < 1.13)
161+
return new ExecutionRequest(descriptor, listener, params);
162+
}
163+
164+
/**
165+
* Check if this factory is using reflection to propagate execution context.
166+
* This is primarily for testing and debugging purposes.
167+
*
168+
* @return true if reflection is being used, false if using public API only
169+
*/
170+
static boolean isUsingReflection() {
171+
return USE_REFLECTION;
172+
}
173+
}

biz.aQute.tester.junit-platform/src/aQute/tester/bundle/engine/discovery/BundleSelectorResolver.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ private BundleSelectorResolver(BundleContext context, EngineDiscoveryRequest req
124124
methodSelectors = request.getSelectorsByType(MethodSelector.class)
125125
.stream()
126126
.map(selector -> DiscoverySelectors.selectMethod(selector.getClassName(), selector.getMethodName(),
127-
selector.getMethodParameterTypes()))
127+
selector.getParameterTypeNames()))
128128
.collect(toList());
129129

130130
bundleSelectors = request.getSelectorsByType(BundleSelector.class);
@@ -436,7 +436,7 @@ private static Optional<Method> findMethod(Class<?> clazz, String method, String
436436
}
437437

438438
private static MethodSelector selectMethod(Class<?> testClass, MethodSelector selector) {
439-
return findMethod(testClass, selector.getMethodName(), selector.getMethodParameterTypes())
439+
return findMethod(testClass, selector.getMethodName(), selector.getParameterTypeNames())
440440
.map(method -> DiscoverySelectors.selectMethod(testClass, method))
441441
.orElse(null);
442442
}

biz.aQute.tester.test/src/aQute/tester/testclasses/junit/platform/JUnit5ContainerError.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
public class JUnit5ContainerError {
77

88
@BeforeAll
9-
void beforeAll() {
9+
static void beforeAll() {
1010
throw new IllegalStateException();
1111
}
1212

biz.aQute.tester.test/src/aQute/tester/testclasses/junit/platform/JUnit5ContainerFailure.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
public class JUnit5ContainerFailure {
77

88
@BeforeAll
9-
void beforeAll() {
9+
static void beforeAll() {
1010
throw new AssertionError();
1111
}
1212

biz.aQute.tester.test/test/aQute/tester/bundle/engine/test/BundleEngineTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import aQute.tester.bundle.engine.BundleDescriptor;
6161
import aQute.tester.bundle.engine.BundleEngine;
6262
import aQute.tester.bundle.engine.BundleEngineDescriptor;
63+
import aQute.tester.bundle.engine.ExecutionRequestFactory;
6364
import aQute.tester.bundle.engine.StaticFailureDescriptor;
6465
import aQute.tester.bundle.engine.discovery.BundleSelector;
6566
import aQute.tester.bundle.engine.discovery.BundleSelectorResolver;
@@ -399,6 +400,9 @@ public Builder engineInFramework() {
399400
.addResourceWithCopy(BundleSelector.class)
400401
.addResourceWithCopy(BundleUtils.class)
401402
.addResourceWithRecursiveCopy(BundleSelectorResolver.class)
403+
// Add ExecutionRequestFactory - needed for JUnit Platform
404+
// 1.13+ support
405+
.addResourceWithCopy(ExecutionRequestFactory.class)
402406
.exportPackage(BundleEngine.class.getPackage()
403407
.getName())
404408
.exportPackage(BundleSelector.class.getPackage()

cnf/ext/junit.bnd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ junit4.eclipse.version=4.13.2
2020
# of junit-platform and the version we're building against.
2121
junit.jupiter.eclipse.version=5.10.1
2222
junit.platform.eclipse.version=1.10.1
23-
junit.jupiter.version=5.12.2
24-
junit.platform.version=1.12.2
23+
junit.jupiter.version=5.14.2
24+
junit.platform.version=1.14.2
2525
opentest4j.version=1.3.0
2626
assertj.version=3.24.2
2727
awaitility.version=4.2.0

gradle-plugins/biz.aQute.bnd.gradle/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ dependencies {
8888
implementation("biz.aQute.bnd:biz.aQute.resolve:${version}")
8989
runtimeOnly("biz.aQute.bnd:biz.aQute.bnd.embedded-repo:${version}")
9090
// keep in sync with cnf/junit.bnd e.g. 'junit.jupiter.version'
91-
testImplementation(enforcedPlatform("org.junit:junit-bom:5.12.2"))
91+
testImplementation(enforcedPlatform("org.junit:junit-bom:5.14.2"))
9292
testImplementation("org.junit.jupiter:junit-jupiter")
9393
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
9494
testImplementation("org.spockframework:spock-core:${spockVersion}")

0 commit comments

Comments
 (0)