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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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