Browse Source
Update the `TestCompiler` so that classes can be defined using a `Lookup`. This update allows package-private classes to be accessed without needing a quite so unusual classloader setup. The `@CompileWithTargetClassAccess` should be added to any test that needs to use `Lookup` based defines. The test will run with a completed forked classloader so not to pollute the main classloader. This commit also adds some useful additional APIs. See gh-28120pull/28422/head
18 changed files with 622 additions and 60 deletions
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.aot.test.generator.compile; |
||||
|
||||
import java.lang.annotation.Documented; |
||||
import java.lang.annotation.ElementType; |
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
import java.lang.invoke.MethodHandles; |
||||
import java.lang.invoke.MethodHandles.Lookup; |
||||
|
||||
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
||||
/** |
||||
* Annotation that can be used on tests that need a {@link TestCompiler} with |
||||
* non-public access to a target class. Allows the compiler to use |
||||
* {@link MethodHandles#privateLookupIn} to {@link Lookup#defineClass define the |
||||
* class} without polluting the test {@link ClassLoader}. |
||||
* |
||||
* @author Phillip Webb |
||||
* @since 6.0 |
||||
*/ |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Target({ ElementType.TYPE, ElementType.METHOD }) |
||||
@Documented |
||||
@ExtendWith(CompileWithTargetClassAccessExtension.class) |
||||
public @interface CompileWithTargetClassAccess { |
||||
|
||||
/** |
||||
* The target class names. |
||||
* @return the class name |
||||
*/ |
||||
String[] classNames() default {}; |
||||
|
||||
/** |
||||
* The target classes. |
||||
* @return the classes |
||||
*/ |
||||
Class<?>[] classes() default {}; |
||||
|
||||
} |
||||
@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.aot.test.generator.compile; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.net.URL; |
||||
import java.util.Enumeration; |
||||
|
||||
/** |
||||
* {@link ClassLoader} implementation to support |
||||
* {@link CompileWithTargetClassAccess @CompileWithTargetClassAccess}. |
||||
* |
||||
* @author Phillip Webb |
||||
* @since 6.0 |
||||
*/ |
||||
final class CompileWithTargetClassAccessClassLoader extends ClassLoader { |
||||
|
||||
private final ClassLoader testClassLoader; |
||||
|
||||
private final String[] targetClasses; |
||||
|
||||
|
||||
public CompileWithTargetClassAccessClassLoader(ClassLoader testClassLoader, |
||||
String[] targetClasses) { |
||||
super(testClassLoader.getParent()); |
||||
this.testClassLoader = testClassLoader; |
||||
this.targetClasses = targetClasses; |
||||
} |
||||
|
||||
|
||||
public String[] getTargetClasses() { |
||||
return this.targetClasses; |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> loadClass(String name) throws ClassNotFoundException { |
||||
if (name.startsWith("org.junit") || name.startsWith("org.hamcrest")) { |
||||
return Class.forName(name, false, this.testClassLoader); |
||||
} |
||||
return super.loadClass(name); |
||||
} |
||||
|
||||
@Override |
||||
protected Class<?> findClass(String name) throws ClassNotFoundException { |
||||
String resourceName = name.replace(".", "/") + ".class"; |
||||
InputStream stream = this.testClassLoader.getResourceAsStream(resourceName); |
||||
if (stream != null) { |
||||
try (stream) { |
||||
byte[] bytes = stream.readAllBytes(); |
||||
return defineClass(name, bytes, 0, bytes.length, null); |
||||
} |
||||
catch (IOException ex) { |
||||
} |
||||
} |
||||
return super.findClass(name); |
||||
} |
||||
|
||||
@Override |
||||
protected Enumeration<URL> findResources(String name) throws IOException { |
||||
return this.testClassLoader.getResources(name); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,197 @@
@@ -0,0 +1,197 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.aot.test.generator.compile; |
||||
|
||||
import java.lang.reflect.AnnotatedElement; |
||||
import java.lang.reflect.Method; |
||||
import java.util.Arrays; |
||||
import java.util.LinkedHashSet; |
||||
import java.util.Set; |
||||
|
||||
import org.junit.jupiter.api.extension.ExtensionContext; |
||||
import org.junit.jupiter.api.extension.InvocationInterceptor; |
||||
import org.junit.jupiter.api.extension.ReflectiveInvocationContext; |
||||
import org.junit.platform.engine.discovery.DiscoverySelectors; |
||||
import org.junit.platform.launcher.Launcher; |
||||
import org.junit.platform.launcher.LauncherDiscoveryRequest; |
||||
import org.junit.platform.launcher.TestPlan; |
||||
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; |
||||
import org.junit.platform.launcher.core.LauncherFactory; |
||||
import org.junit.platform.launcher.listeners.SummaryGeneratingListener; |
||||
import org.junit.platform.launcher.listeners.TestExecutionSummary; |
||||
|
||||
import org.springframework.core.annotation.MergedAnnotation; |
||||
import org.springframework.core.annotation.MergedAnnotations; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.CollectionUtils; |
||||
import org.springframework.util.ReflectionUtils; |
||||
|
||||
/** |
||||
* JUnit {@link InvocationInterceptor} to support |
||||
* {@link CompileWithTargetClassAccess @CompileWithTargetClassAccess}. |
||||
* |
||||
* @author Christoph Dreis |
||||
* @author Phillip Webb |
||||
* @since 6.0 |
||||
*/ |
||||
class CompileWithTargetClassAccessExtension implements InvocationInterceptor { |
||||
|
||||
@Override |
||||
public void interceptBeforeAllMethod(Invocation<Void> invocation, |
||||
ReflectiveInvocationContext<Method> invocationContext, |
||||
ExtensionContext extensionContext) throws Throwable { |
||||
|
||||
intercept(invocation, extensionContext); |
||||
} |
||||
|
||||
@Override |
||||
public void interceptBeforeEachMethod(Invocation<Void> invocation, |
||||
ReflectiveInvocationContext<Method> invocationContext, |
||||
ExtensionContext extensionContext) throws Throwable { |
||||
|
||||
intercept(invocation, extensionContext); |
||||
} |
||||
|
||||
@Override |
||||
public void interceptAfterEachMethod(Invocation<Void> invocation, |
||||
ReflectiveInvocationContext<Method> invocationContext, |
||||
ExtensionContext extensionContext) throws Throwable { |
||||
|
||||
intercept(invocation, extensionContext); |
||||
} |
||||
|
||||
@Override |
||||
public void interceptAfterAllMethod(Invocation<Void> invocation, |
||||
ReflectiveInvocationContext<Method> invocationContext, |
||||
ExtensionContext extensionContext) throws Throwable { |
||||
|
||||
intercept(invocation, extensionContext); |
||||
} |
||||
|
||||
@Override |
||||
public void interceptTestMethod(Invocation<Void> invocation, |
||||
ReflectiveInvocationContext<Method> invocationContext, |
||||
ExtensionContext extensionContext) throws Throwable { |
||||
|
||||
intercept(invocation, extensionContext, |
||||
() -> runTestWithModifiedClassPath(invocationContext, extensionContext)); |
||||
} |
||||
|
||||
private void intercept(Invocation<Void> invocation, ExtensionContext extensionContext) |
||||
throws Throwable { |
||||
|
||||
intercept(invocation, extensionContext, Action.NONE); |
||||
} |
||||
|
||||
private void intercept(Invocation<Void> invocation, ExtensionContext extensionContext, |
||||
Action action) throws Throwable { |
||||
|
||||
if (isUsingForkedClassPathLoader(extensionContext)) { |
||||
invocation.proceed(); |
||||
return; |
||||
} |
||||
invocation.skip(); |
||||
action.run(); |
||||
} |
||||
|
||||
private boolean isUsingForkedClassPathLoader(ExtensionContext extensionContext) { |
||||
Class<?> testClass = extensionContext.getRequiredTestClass(); |
||||
ClassLoader classLoader = testClass.getClassLoader(); |
||||
return classLoader.getClass().getName() |
||||
.equals(CompileWithTargetClassAccessClassLoader.class.getName()); |
||||
} |
||||
|
||||
private void runTestWithModifiedClassPath( |
||||
ReflectiveInvocationContext<Method> invocationContext, |
||||
ExtensionContext extensionContext) throws Throwable { |
||||
|
||||
Class<?> testClass = extensionContext.getRequiredTestClass(); |
||||
Method testMethod = invocationContext.getExecutable(); |
||||
String[] targetClasses = getTargetClasses(testClass, testMethod); |
||||
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); |
||||
ClassLoader forkedClassPathClassLoader = new CompileWithTargetClassAccessClassLoader( |
||||
testClass.getClassLoader(), targetClasses); |
||||
Thread.currentThread().setContextClassLoader(forkedClassPathClassLoader); |
||||
try { |
||||
runTest(forkedClassPathClassLoader, testClass.getName(), testMethod.getName()); |
||||
} |
||||
finally { |
||||
Thread.currentThread().setContextClassLoader(originalClassLoader); |
||||
} |
||||
} |
||||
|
||||
private String[] getTargetClasses(AnnotatedElement... elements) { |
||||
Set<String> targetClasses = new LinkedHashSet<>(); |
||||
for (AnnotatedElement element : elements) { |
||||
MergedAnnotation<?> annotation = MergedAnnotations.from(element) |
||||
.get(CompileWithTargetClassAccess.class); |
||||
if (annotation.isPresent()) { |
||||
Arrays.stream(annotation.getStringArray("classNames")).forEach(targetClasses::add); |
||||
Arrays.stream(annotation.getClassArray("classes")).map(Class::getName).forEach(targetClasses::add); |
||||
if (element instanceof Class<?> clazz) { |
||||
targetClasses.add(clazz.getName()); |
||||
} |
||||
} |
||||
} |
||||
return targetClasses.toArray(String[]::new); |
||||
} |
||||
|
||||
private void runTest(ClassLoader classLoader, String testClassName, |
||||
String testMethodName) throws Throwable { |
||||
|
||||
Class<?> testClass = classLoader.loadClass(testClassName); |
||||
Method testMethod = findMethod(testClass, testMethodName); |
||||
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() |
||||
.selectors(DiscoverySelectors.selectMethod(testClass, testMethod)) |
||||
.build(); |
||||
Launcher launcher = LauncherFactory.create(); |
||||
TestPlan testPlan = launcher.discover(request); |
||||
SummaryGeneratingListener listener = new SummaryGeneratingListener(); |
||||
launcher.registerTestExecutionListeners(listener); |
||||
launcher.execute(testPlan); |
||||
TestExecutionSummary summary = listener.getSummary(); |
||||
if (!CollectionUtils.isEmpty(summary.getFailures())) { |
||||
throw summary.getFailures().get(0).getException(); |
||||
} |
||||
} |
||||
|
||||
private Method findMethod(Class<?> testClass, String testMethodName) { |
||||
Method method = ReflectionUtils.findMethod(testClass, testMethodName); |
||||
if (method == null) { |
||||
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(testClass); |
||||
for (Method candidate : methods) { |
||||
if (candidate.getName().equals(testMethodName)) { |
||||
return candidate; |
||||
} |
||||
} |
||||
} |
||||
Assert.state(method != null, () -> "Unable to find " + testClass + "." + testMethodName); |
||||
return method; |
||||
} |
||||
|
||||
|
||||
interface Action { |
||||
|
||||
static Action NONE = () -> { |
||||
}; |
||||
|
||||
|
||||
void run() throws Throwable; |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue