mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-09-03 04:26:12 +08:00
Support modified classpath on methods and parameterized tests
Update `ModifiedClassPathExtension` and related classes so that annotations can be used directly on test methods, or on classes passed into parameterized tests. Closes gh-33014
This commit is contained in:
parent
d00e004622
commit
d4cc8fc3a6
@ -32,7 +32,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
* @since 1.5.0
|
* @since 1.5.0
|
||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.TYPE)
|
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||||
@Documented
|
@Documented
|
||||||
@ExtendWith(ModifiedClassPathExtension.class)
|
@ExtendWith(ModifiedClassPathExtension.class)
|
||||||
public @interface ClassPathExclusions {
|
public @interface ClassPathExclusions {
|
||||||
|
@ -31,7 +31,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
* @since 1.5.0
|
* @since 1.5.0
|
||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.TYPE)
|
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||||
@Documented
|
@Documented
|
||||||
@ExtendWith(ModifiedClassPathExtension.class)
|
@ExtendWith(ModifiedClassPathExtension.class)
|
||||||
public @interface ClassPathOverrides {
|
public @interface ClassPathOverrides {
|
||||||
|
@ -25,15 +25,15 @@ import java.lang.annotation.Target;
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotation used to fork the classpath. This can be helpful where neither
|
* Annotation used to fork the classpath. This can be helpful when using annotations on
|
||||||
* {@link ClassPathExclusions} or {@link ClassPathOverrides} are needed, but just a copy
|
* parameterized tests, or where neither {@link ClassPathExclusions} or
|
||||||
* of the classpath.
|
* {@link ClassPathOverrides} are needed, but just a copy of the classpath.
|
||||||
*
|
*
|
||||||
* @author Christoph Dreis
|
* @author Christoph Dreis
|
||||||
* @since 2.4.0
|
* @since 2.4.0
|
||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.TYPE)
|
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||||
@Documented
|
@Documented
|
||||||
@ExtendWith(ModifiedClassPathExtension.class)
|
@ExtendWith(ModifiedClassPathExtension.class)
|
||||||
public @interface ForkedClassPath {
|
public @interface ForkedClassPath {
|
||||||
|
@ -18,17 +18,22 @@ package org.springframework.boot.testsupport.classpath;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.lang.management.ManagementFactory;
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.reflect.AnnotatedElement;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.jar.Attributes;
|
import java.util.jar.Attributes;
|
||||||
import java.util.jar.JarFile;
|
import java.util.jar.JarFile;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
|
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
|
||||||
@ -52,6 +57,7 @@ import org.springframework.core.annotation.MergedAnnotation;
|
|||||||
import org.springframework.core.annotation.MergedAnnotations;
|
import org.springframework.core.annotation.MergedAnnotations;
|
||||||
import org.springframework.util.AntPathMatcher;
|
import org.springframework.util.AntPathMatcher;
|
||||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,7 +68,7 @@ import org.springframework.util.StringUtils;
|
|||||||
*/
|
*/
|
||||||
final class ModifiedClassPathClassLoader extends URLClassLoader {
|
final class ModifiedClassPathClassLoader extends URLClassLoader {
|
||||||
|
|
||||||
private static final Map<Class<?>, ModifiedClassPathClassLoader> cache = new ConcurrentReferenceHashMap<>();
|
private static final Map<List<AnnotatedElement>, ModifiedClassPathClassLoader> cache = new ConcurrentReferenceHashMap<>();
|
||||||
|
|
||||||
private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar");
|
private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar");
|
||||||
|
|
||||||
@ -84,19 +90,44 @@ final class ModifiedClassPathClassLoader extends URLClassLoader {
|
|||||||
return super.loadClass(name);
|
return super.loadClass(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ModifiedClassPathClassLoader get(Class<?> testClass) {
|
static ModifiedClassPathClassLoader get(Class<?> testClass, Method testMethod, List<Object> arguments) {
|
||||||
return cache.computeIfAbsent(testClass, ModifiedClassPathClassLoader::compute);
|
Set<AnnotatedElement> candidates = new LinkedHashSet<>();
|
||||||
|
candidates.add(testClass);
|
||||||
|
candidates.add(testMethod);
|
||||||
|
candidates.addAll(getAnnotatedElements(arguments.toArray()));
|
||||||
|
List<AnnotatedElement> annotatedElements = candidates.stream()
|
||||||
|
.filter(ModifiedClassPathClassLoader::hasAnnotation).collect(Collectors.toList());
|
||||||
|
if (annotatedElements.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cache.computeIfAbsent(annotatedElements, (key) -> compute(testClass.getClassLoader(), key));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ModifiedClassPathClassLoader compute(Class<?> testClass) {
|
private static Collection<AnnotatedElement> getAnnotatedElements(Object[] array) {
|
||||||
ClassLoader classLoader = testClass.getClassLoader();
|
Set<AnnotatedElement> result = new LinkedHashSet<>();
|
||||||
MergedAnnotations annotations = MergedAnnotations.from(testClass,
|
for (Object item : array) {
|
||||||
MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
|
if (item instanceof AnnotatedElement) {
|
||||||
if (annotations.isPresent(ForkedClassPath.class) && (annotations.isPresent(ClassPathOverrides.class)
|
result.add((AnnotatedElement) item);
|
||||||
|| annotations.isPresent(ClassPathExclusions.class))) {
|
}
|
||||||
throw new IllegalStateException("@ForkedClassPath is redundant in combination with either "
|
else if (ObjectUtils.isArray(item)) {
|
||||||
+ "@ClassPathOverrides or @ClassPathExclusions");
|
result.addAll(getAnnotatedElements(ObjectUtils.toObjectArray(item)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasAnnotation(AnnotatedElement element) {
|
||||||
|
MergedAnnotations annotations = MergedAnnotations.from(element,
|
||||||
|
MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
|
||||||
|
return annotations.isPresent(ForkedClassPath.class) || annotations.isPresent(ClassPathOverrides.class)
|
||||||
|
|| annotations.isPresent(ClassPathExclusions.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModifiedClassPathClassLoader compute(ClassLoader classLoader,
|
||||||
|
List<AnnotatedElement> annotatedClasses) {
|
||||||
|
List<MergedAnnotations> annotations = annotatedClasses.stream()
|
||||||
|
.map((source) -> MergedAnnotations.from(source, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY))
|
||||||
|
.collect(Collectors.toList());
|
||||||
return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), annotations),
|
return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), annotations),
|
||||||
classLoader.getParent(), classLoader);
|
classLoader.getParent(), classLoader);
|
||||||
}
|
}
|
||||||
@ -174,9 +205,9 @@ final class ModifiedClassPathClassLoader extends URLClassLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static URL[] processUrls(URL[] urls, MergedAnnotations annotations) {
|
private static URL[] processUrls(URL[] urls, List<MergedAnnotations> annotations) {
|
||||||
ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations.get(ClassPathExclusions.class));
|
ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations);
|
||||||
List<URL> additionalUrls = getAdditionalUrls(annotations.get(ClassPathOverrides.class));
|
List<URL> additionalUrls = getAdditionalUrls(annotations);
|
||||||
List<URL> processedUrls = new ArrayList<>(additionalUrls);
|
List<URL> processedUrls = new ArrayList<>(additionalUrls);
|
||||||
for (URL url : urls) {
|
for (URL url : urls) {
|
||||||
if (!filter.isExcluded(url)) {
|
if (!filter.isExcluded(url)) {
|
||||||
@ -186,11 +217,15 @@ final class ModifiedClassPathClassLoader extends URLClassLoader {
|
|||||||
return processedUrls.toArray(new URL[0]);
|
return processedUrls.toArray(new URL[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<URL> getAdditionalUrls(MergedAnnotation<ClassPathOverrides> annotation) {
|
private static List<URL> getAdditionalUrls(List<MergedAnnotations> annotations) {
|
||||||
if (!annotation.isPresent()) {
|
Set<URL> urls = new LinkedHashSet<>();
|
||||||
return Collections.emptyList();
|
for (MergedAnnotations candidate : annotations) {
|
||||||
|
MergedAnnotation<ClassPathOverrides> annotation = candidate.get(ClassPathOverrides.class);
|
||||||
|
if (annotation.isPresent()) {
|
||||||
|
urls.addAll(resolveCoordinates(annotation.getStringArray(MergedAnnotation.VALUE)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return resolveCoordinates(annotation.getStringArray(MergedAnnotation.VALUE));
|
return urls.stream().collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<URL> resolveCoordinates(String[] coordinates) {
|
private static List<URL> resolveCoordinates(String[] coordinates) {
|
||||||
@ -241,9 +276,15 @@ final class ModifiedClassPathClassLoader extends URLClassLoader {
|
|||||||
|
|
||||||
private final AntPathMatcher matcher = new AntPathMatcher();
|
private final AntPathMatcher matcher = new AntPathMatcher();
|
||||||
|
|
||||||
private ClassPathEntryFilter(MergedAnnotation<ClassPathExclusions> annotation) {
|
private ClassPathEntryFilter(List<MergedAnnotations> annotations) {
|
||||||
this.exclusions = annotation.getValue(MergedAnnotation.VALUE, String[].class).map(Arrays::asList)
|
Set<String> exclusions = new LinkedHashSet<>();
|
||||||
.orElse(Collections.emptyList());
|
for (MergedAnnotations candidate : annotations) {
|
||||||
|
MergedAnnotation<ClassPathExclusions> annotation = candidate.get(ClassPathExclusions.class);
|
||||||
|
if (annotation.isPresent()) {
|
||||||
|
exclusions.addAll(Arrays.asList(annotation.getStringArray(MergedAnnotation.VALUE)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.exclusions = exclusions.stream().collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isExcluded(URL url) {
|
private boolean isExcluded(URL url) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2012-2021 the original author or authors.
|
* Copyright 2012-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -46,7 +46,7 @@ import org.springframework.util.ReflectionUtils;
|
|||||||
*
|
*
|
||||||
* @author Christoph Dreis
|
* @author Christoph Dreis
|
||||||
*/
|
*/
|
||||||
class ModifiedClassPathExtension implements InvocationInterceptor {
|
public class ModifiedClassPathExtension implements InvocationInterceptor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void interceptBeforeAllMethod(Invocation<Void> invocation,
|
public void interceptBeforeAllMethod(Invocation<Void> invocation,
|
||||||
@ -75,20 +75,31 @@ class ModifiedClassPathExtension implements InvocationInterceptor {
|
|||||||
@Override
|
@Override
|
||||||
public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext,
|
public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext,
|
||||||
ExtensionContext extensionContext) throws Throwable {
|
ExtensionContext extensionContext) throws Throwable {
|
||||||
|
interceptMethod(invocation, invocationContext, extensionContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void interceptTestTemplateMethod(Invocation<Void> invocation,
|
||||||
|
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
|
||||||
|
interceptMethod(invocation, invocationContext, extensionContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void interceptMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext,
|
||||||
|
ExtensionContext extensionContext) throws Throwable {
|
||||||
if (isModifiedClassPathClassLoader(extensionContext)) {
|
if (isModifiedClassPathClassLoader(extensionContext)) {
|
||||||
invocation.proceed();
|
invocation.proceed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
invocation.skip();
|
|
||||||
runTestWithModifiedClassPath(invocationContext, extensionContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runTestWithModifiedClassPath(ReflectiveInvocationContext<Method> invocationContext,
|
|
||||||
ExtensionContext extensionContext) throws Throwable {
|
|
||||||
Class<?> testClass = extensionContext.getRequiredTestClass();
|
Class<?> testClass = extensionContext.getRequiredTestClass();
|
||||||
Method testMethod = invocationContext.getExecutable();
|
Method testMethod = invocationContext.getExecutable();
|
||||||
|
URLClassLoader modifiedClassLoader = ModifiedClassPathClassLoader.get(testClass, testMethod,
|
||||||
|
invocationContext.getArguments());
|
||||||
|
if (modifiedClassLoader == null) {
|
||||||
|
invocation.proceed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invocation.skip();
|
||||||
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
|
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
|
||||||
URLClassLoader modifiedClassLoader = ModifiedClassPathClassLoader.get(testClass);
|
|
||||||
Thread.currentThread().setContextClassLoader(modifiedClassLoader);
|
Thread.currentThread().setContextClassLoader(modifiedClassLoader);
|
||||||
try {
|
try {
|
||||||
runTest(modifiedClassLoader, testClass.getName(), testMethod.getName());
|
runTest(modifiedClassLoader, testClass.getName(), testMethod.getName());
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2019 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.boot.testsupport.classpath;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ModifiedClassPathExtension} overriding entries on the class path via
|
||||||
|
* parameters.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ModifiedClassPathExtensionOverridesParametrizedTests {
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ForkedClassPath
|
||||||
|
@MethodSource("parameter")
|
||||||
|
void classesAreLoadedFromParameter(Class<?> type) {
|
||||||
|
assertThat(ApplicationContext.class.getProtectionDomain().getCodeSource().getLocation().toString())
|
||||||
|
.endsWith("spring-context-4.1.0.RELEASE.jar");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Class<?>[] parameter() {
|
||||||
|
return new Class<?>[] { ClassWithOverride.class };
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ForkedClassPath
|
||||||
|
@MethodSource("arrayParameter")
|
||||||
|
void classesAreLoadedFromParameterInArray(Object[] types) {
|
||||||
|
assertThat(ApplicationContext.class.getProtectionDomain().getCodeSource().getLocation().toString())
|
||||||
|
.endsWith("spring-context-4.1.0.RELEASE.jar");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<Arguments> arrayParameter() {
|
||||||
|
Object[] types = new Object[] { ClassWithOverride.class };
|
||||||
|
return Stream.of(Arguments.of(new Object[] { types }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ClassPathOverrides("org.springframework:spring-context:4.1.0.RELEASE")
|
||||||
|
static class ClassWithOverride {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user