From a6c5d438fe66803bab43dcab97b27076c0a44567 Mon Sep 17 00:00:00 2001 From: Krishna Date: Mon, 23 Jan 2023 16:16:30 +0530 Subject: [PATCH] Removed ModifiedClasspathRunner and used the ClasspathExtensions from the Spring boot test support (#1181) --- spring-cloud-commons/pom.xml | 5 - .../ManagementServerPortUtilsTests.java | 5 +- ...actLoadBalancerAutoConfigurationTests.java | 2 +- .../LoadBalancerAutoConfigurationTests.java | 4 - ...ServiceRegistryAutoConfigurationTests.java | 5 +- spring-cloud-context/pom.xml | 5 - ...efreshAutoConfigurationClassPathTests.java | 5 +- ...shAutoConfigurationMoreClassPathTests.java | 18 +- .../bootstrap/encrypt/RsaDisabledTests.java | 13 +- spring-cloud-loadbalancer/pom.xml | 5 - ...dBalancerClientAutoConfigurationTests.java | 25 +- spring-cloud-starter-bootstrap/pom.xml | 4 + spring-cloud-test-support/pom.xml | 12 + .../cloud/test/ClassPathExclusions.java | 3 + .../cloud/test/ClassPathOverrides.java | 5 +- .../test/ModifiedClassPathClassLoader.java | 310 ++++++++++++++++++ .../test/ModifiedClassPathExtension.java | 140 ++++++++ .../cloud/test/ModifiedClassPathRunner.java | 4 + ...odifiedClassPathRunnerExclusionsTests.java | 6 +- ...ModifiedClassPathRunnerOverridesTests.java | 6 +- 20 files changed, 505 insertions(+), 77 deletions(-) create mode 100644 spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathClassLoader.java create mode 100644 spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathExtension.java diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml index 19d15573..beab8ce8 100644 --- a/spring-cloud-commons/pom.xml +++ b/spring-cloud-commons/pom.xml @@ -167,11 +167,6 @@ spring-boot-starter-test test - - org.junit.vintage - junit-vintage-engine - test - org.springframework.cloud spring-cloud-test-support diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/ManagementServerPortUtilsTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/ManagementServerPortUtilsTests.java index 520cb185..a7c9e2e9 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/ManagementServerPortUtilsTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/ManagementServerPortUtilsTests.java @@ -16,20 +16,17 @@ package org.springframework.cloud.client.discovery; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.test.ClassPathExclusions; -import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.context.ConfigurableApplicationContext; import static org.assertj.core.api.BDDAssertions.then; -@RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions({ "spring-boot-actuator-autoconfigure-*" }) public class ManagementServerPortUtilsTests { diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java index 1a0d21b8..a4be354d 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java @@ -21,7 +21,7 @@ import java.util.Collection; import java.util.Map; import java.util.Random; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.WebApplicationType; diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java index 6a92b5f1..40436e92 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java @@ -18,10 +18,7 @@ package org.springframework.cloud.client.loadbalancer; import java.util.List; -import org.junit.runner.RunWith; - import org.springframework.cloud.test.ClassPathExclusions; -import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; @@ -30,7 +27,6 @@ import static org.assertj.core.api.BDDAssertions.then; /** * @author Spencer Gibb */ -@RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions({ "spring-retry-*.jar", "spring-boot-starter-aop-*.jar" }) public class LoadBalancerAutoConfigurationTests extends AbstractLoadBalancerAutoConfigurationTests { diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/serviceregistry/ServiceRegistryAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/serviceregistry/ServiceRegistryAutoConfigurationTests.java index 0cccbae5..3e919ecc 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/serviceregistry/ServiceRegistryAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/serviceregistry/ServiceRegistryAutoConfigurationTests.java @@ -16,14 +16,12 @@ package org.springframework.cloud.client.serviceregistry; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.test.ClassPathExclusions; -import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; @@ -32,7 +30,6 @@ import static org.assertj.core.api.Assertions.fail; /** * @author Spencer Gibb */ -@RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions({ "spring-boot-actuator-*.jar", "spring-boot-starter-actuator-*.jar" }) public class ServiceRegistryAutoConfigurationTests { diff --git a/spring-cloud-context/pom.xml b/spring-cloud-context/pom.xml index d072cc28..1c6ad521 100644 --- a/spring-cloud-context/pom.xml +++ b/spring-cloud-context/pom.xml @@ -72,11 +72,6 @@ spring-boot-starter-test test - - org.junit.vintage - junit-vintage-engine - test - org.springframework.cloud spring-cloud-test-support diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationClassPathTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationClassPathTests.java index 0ae9196c..86e16a76 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationClassPathTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationClassPathTests.java @@ -16,15 +16,13 @@ package org.springframework.cloud.autoconfigure; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.endpoint.event.RefreshEventListener; import org.springframework.cloud.test.ClassPathExclusions; -import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; @@ -33,7 +31,6 @@ import static org.assertj.core.api.BDDAssertions.then; /** * @author Spencer Gibb */ -@RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions({ "spring-boot-actuator-*.jar", "spring-boot-starter-actuator-*.jar" }) public class RefreshAutoConfigurationClassPathTests { diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationMoreClassPathTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationMoreClassPathTests.java index 70492ba8..664d12f5 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationMoreClassPathTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationMoreClassPathTests.java @@ -16,16 +16,15 @@ package org.springframework.cloud.autoconfigure; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.test.system.OutputCaptureRule; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.test.ClassPathExclusions; -import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; @@ -34,21 +33,18 @@ import static org.assertj.core.api.BDDAssertions.then; /** * @author Spencer Gibb */ -@RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions({ "spring-boot-actuator-autoconfigure-*.jar", "spring-boot-starter-actuator-*.jar" }) +@ExtendWith(OutputCaptureExtension.class) public class RefreshAutoConfigurationMoreClassPathTests { - @Rule - public OutputCaptureRule outputCapture = new OutputCaptureRule(); - private static ConfigurableApplicationContext getApplicationContext(Class configuration, String... properties) { return new SpringApplicationBuilder(configuration).web(WebApplicationType.NONE).properties(properties).run(); } @Test - public void unknownClassProtected() { + public void unknownClassProtected(CapturedOutput outputCapture) { try (ConfigurableApplicationContext context = getApplicationContext(Config.class, "debug=true")) { - String output = this.outputCapture.toString(); + String output = outputCapture.toString(); then(output) .doesNotContain("Failed to introspect annotations on " + "[class org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration") diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/RsaDisabledTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/RsaDisabledTests.java index 7bc03db4..80056cf4 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/RsaDisabledTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/RsaDisabledTests.java @@ -18,15 +18,13 @@ package org.springframework.cloud.bootstrap.encrypt; import java.util.Map; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.test.ClassPathExclusions; -import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.context.ConfigurableApplicationContext; import static org.assertj.core.api.BDDAssertions.then; @@ -34,20 +32,19 @@ import static org.assertj.core.api.BDDAssertions.then; /** * @author Ryan Baxter */ -@RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions({ "spring-security-rsa*.jar" }) public class RsaDisabledTests { private ConfigurableApplicationContext context; - @Before + @BeforeEach public void setUp() { this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) .sources(EncryptionBootstrapConfiguration.class).web(WebApplicationType.NONE) .properties("encrypt.key:mykey", "encrypt.rsa.strong:true", "encrypt.rsa.salt:foobar").run(); } - @After + @AfterEach public void tearDown() { if (this.context != null) { this.context.close(); diff --git a/spring-cloud-loadbalancer/pom.xml b/spring-cloud-loadbalancer/pom.xml index 709d1b2c..8bf44ba7 100644 --- a/spring-cloud-loadbalancer/pom.xml +++ b/spring-cloud-loadbalancer/pom.xml @@ -138,10 +138,5 @@ awaitility test - - org.junit.vintage - junit-vintage-engine - test - diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfigurationTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfigurationTests.java index 5490f8f2..2101ac40 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfigurationTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfigurationTests.java @@ -17,37 +17,28 @@ package org.springframework.cloud.loadbalancer.security; import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.test.ClassPathExclusions; -import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; - /** * @author Dave Syer * */ -@RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions("spring-retry-*.jar") public class OAuth2LoadBalancerClientAutoConfigurationTests { private ConfigurableApplicationContext context; - @Rule - public ExpectedException expected = ExpectedException.none(); - - @Before + @BeforeEach public void before() { // FIXME: why do I need to do this? (fails in maven build without it. // https://stackoverflow.com/questions/28911560/tomcat-8-embedded-error-org-apache-catalina-core-containerbase-a-child-con @@ -55,7 +46,7 @@ public class OAuth2LoadBalancerClientAutoConfigurationTests { TomcatURLStreamHandlerFactory.disable(); } - @After + @AfterEach public void close() { if (this.context != null) { this.context.close(); @@ -63,7 +54,7 @@ public class OAuth2LoadBalancerClientAutoConfigurationTests { } @Test - @Ignore + @Disabled public void userInfoNotLoadBalanced() { this.context = new SpringApplicationBuilder(ClientConfiguration.class).properties("spring.config.name=test", "server.port=0", "security.oauth2.resource.userInfoUri:https://example.com").run(); @@ -73,7 +64,7 @@ public class OAuth2LoadBalancerClientAutoConfigurationTests { } @Test - @Ignore + @Disabled public void userInfoLoadBalancedNoRetry() { this.context = new SpringApplicationBuilder(ClientConfiguration.class).properties("spring.config.name=test", "server.port=0", "security.oauth2.resource.userInfoUri:https://nosuchservice", diff --git a/spring-cloud-starter-bootstrap/pom.xml b/spring-cloud-starter-bootstrap/pom.xml index 968dc45f..3185fc35 100644 --- a/spring-cloud-starter-bootstrap/pom.xml +++ b/spring-cloud-starter-bootstrap/pom.xml @@ -27,6 +27,10 @@ org.springframework.cloud spring-cloud-starter + + org.junit.platform + junit-platform-launcher + org.springframework.boot spring-boot-starter-test diff --git a/spring-cloud-test-support/pom.xml b/spring-cloud-test-support/pom.xml index 51c237a7..f1620cf2 100644 --- a/spring-cloud-test-support/pom.xml +++ b/spring-cloud-test-support/pom.xml @@ -65,6 +65,18 @@ spring-boot-starter-test test + + org.junit.platform + junit-platform-engine + + + org.junit.platform + junit-platform-launcher + + + org.junit.jupiter + junit-jupiter + org.hibernate.validator hibernate-validator diff --git a/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ClassPathExclusions.java b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ClassPathExclusions.java index 2609cf08..075ec66b 100644 --- a/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ClassPathExclusions.java +++ b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ClassPathExclusions.java @@ -21,6 +21,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; + /** * Taken from Spring Boot test utils. https://github.com/spring-projects/spring-boot/blob/ * 1.4.x/spring-boot/src/test/java/org/springframework/boot/testutil/ClassPathExclusions.java @@ -28,6 +30,7 @@ import java.lang.annotation.Target; */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) +@ExtendWith(ModifiedClassPathExtension.class) public @interface ClassPathExclusions { /** diff --git a/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ClassPathOverrides.java b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ClassPathOverrides.java index 237f21f1..291c0670 100644 --- a/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ClassPathOverrides.java +++ b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ClassPathOverrides.java @@ -21,14 +21,17 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; + /** - * Annotation used in combination with {@link ModifiedClassPathRunner} to override entries + * Annotation used in combination with {@link ModifiedClassPathExtension} to override entries * on the classpath. * * @author Andy Wilkinson */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) +@ExtendWith(ModifiedClassPathExtension.class) public @interface ClassPathOverrides { /** diff --git a/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathClassLoader.java b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathClassLoader.java new file mode 100644 index 00000000..df044250 --- /dev/null +++ b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathClassLoader.java @@ -0,0 +1,310 @@ +/* + * Copyright 2012-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.cloud.test; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.impl.DefaultServiceLocator; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.DependencyRequest; +import org.eclipse.aether.resolution.DependencyResult; +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transport.http.HttpTransporterFactory; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Custom {@link URLClassLoader} that modifies the class path. + * + * @author Andy Wilkinson + * @author Christoph Dreis + * @author Siva Krishna Battu + * @see ModifiedClassPathClassLoader.java + */ +final class ModifiedClassPathClassLoader extends URLClassLoader { + + private static final Map, ModifiedClassPathClassLoader> cache = new ConcurrentReferenceHashMap<>(); + + private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar"); + + private static final int MAX_RESOLUTION_ATTEMPTS = 5; + + private final ClassLoader junitLoader; + + ModifiedClassPathClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) { + super(urls, parent); + this.junitLoader = junitLoader; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (name.startsWith("org.junit") || name.startsWith("org.hamcrest") + || name.startsWith("io.netty.internal.tcnative")) { + return Class.forName(name, false, this.junitLoader); + } + return super.loadClass(name); + } + + static ModifiedClassPathClassLoader get(Class testClass, Method testMethod, List arguments) { + Set candidates = new LinkedHashSet<>(); + candidates.add(testClass); + candidates.add(testMethod); + candidates.addAll(getAnnotatedElements(arguments.toArray())); + List 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 Collection getAnnotatedElements(Object[] array) { + Set result = new LinkedHashSet<>(); + for (Object item : array) { + if (item instanceof AnnotatedElement) { + result.add((AnnotatedElement) item); + } + else if (ObjectUtils.isArray(item)) { + 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(ClassPathOverrides.class) || annotations.isPresent(ClassPathExclusions.class); + } + + private static ModifiedClassPathClassLoader compute(ClassLoader classLoader, + List annotatedClasses) { + List annotations = annotatedClasses.stream() + .map((source) -> MergedAnnotations.from(source, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)) + .toList(); + return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), annotations), + classLoader.getParent(), classLoader); + } + + private static URL[] extractUrls(ClassLoader classLoader) { + List extractedUrls = new ArrayList<>(); + doExtractUrls(classLoader).forEach((URL url) -> { + if (isManifestOnlyJar(url)) { + extractedUrls.addAll(extractUrlsFromManifestClassPath(url)); + } + else { + extractedUrls.add(url); + } + }); + return extractedUrls.toArray(new URL[0]); + } + + private static Stream doExtractUrls(ClassLoader classLoader) { + if (classLoader instanceof URLClassLoader urlClassLoader) { + return Stream.of(urlClassLoader.getURLs()); + } + return Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) + .map(ModifiedClassPathClassLoader::toURL); + } + + private static URL toURL(String entry) { + try { + return new File(entry).toURI().toURL(); + } + catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + + private static boolean isManifestOnlyJar(URL url) { + return isShortenedIntelliJJar(url); + } + + private static boolean isShortenedIntelliJJar(URL url) { + String urlPath = url.getPath(); + boolean isCandidate = INTELLIJ_CLASSPATH_JAR_PATTERN.matcher(urlPath).matches(); + if (isCandidate) { + try { + Attributes attributes = getManifestMainAttributesFromUrl(url); + String createdBy = attributes.getValue("Created-By"); + return createdBy != null && createdBy.contains("IntelliJ"); + } + catch (Exception ex) { + } + } + return false; + } + + private static List extractUrlsFromManifestClassPath(URL booterJar) { + List urls = new ArrayList<>(); + try { + for (String entry : getClassPath(booterJar)) { + urls.add(new URL(entry)); + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + return urls; + } + + private static String[] getClassPath(URL booterJar) throws Exception { + Attributes attributes = getManifestMainAttributesFromUrl(booterJar); + return StringUtils.delimitedListToStringArray(attributes.getValue(Attributes.Name.CLASS_PATH), " "); + } + + private static Attributes getManifestMainAttributesFromUrl(URL url) throws Exception { + try (JarFile jarFile = new JarFile(new File(url.toURI()))) { + return jarFile.getManifest().getMainAttributes(); + } + } + + private static URL[] processUrls(URL[] urls, List annotations) { + ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations); + List additionalUrls = getAdditionalUrls(annotations); + List processedUrls = new ArrayList<>(additionalUrls); + for (URL url : urls) { + if (!filter.isExcluded(url)) { + processedUrls.add(url); + } + } + return processedUrls.toArray(new URL[0]); + } + + private static List getAdditionalUrls(List annotations) { + Set urls = new LinkedHashSet<>(); + for (MergedAnnotations candidate : annotations) { + MergedAnnotation annotation = candidate.get(ClassPathOverrides.class); + if (annotation.isPresent()) { + urls.addAll(resolveCoordinates(annotation.getStringArray(MergedAnnotation.VALUE))); + } + } + return urls.stream().toList(); + } + + private static List resolveCoordinates(String[] coordinates) { + Exception latestFailure = null; + DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator(); + serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); + serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class); + RepositorySystem repositorySystem = serviceLocator.getService(RepositorySystem.class); + DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); + LocalRepository localRepository = new LocalRepository(System.getProperty("user.home") + "/.m2/repository"); + RemoteRepository remoteRepository = new RemoteRepository.Builder("central", "default", + "https://repo.maven.apache.org/maven2").build(); + session.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(session, localRepository)); + for (int i = 0; i < MAX_RESOLUTION_ATTEMPTS; i++) { + CollectRequest collectRequest = new CollectRequest(null, Arrays.asList(remoteRepository)); + collectRequest.setDependencies(createDependencies(coordinates)); + DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, null); + try { + DependencyResult result = repositorySystem.resolveDependencies(session, dependencyRequest); + List resolvedArtifacts = new ArrayList<>(); + for (ArtifactResult artifact : result.getArtifactResults()) { + resolvedArtifacts.add(artifact.getArtifact().getFile().toURI().toURL()); + } + return resolvedArtifacts; + } + catch (Exception ex) { + latestFailure = ex; + } + } + throw new IllegalStateException("Resolution failed after " + MAX_RESOLUTION_ATTEMPTS + " attempts", + latestFailure); + } + + private static List createDependencies(String[] allCoordinates) { + List dependencies = new ArrayList<>(); + for (String coordinate : allCoordinates) { + dependencies.add(new Dependency(new DefaultArtifact(coordinate), null)); + } + return dependencies; + } + + /** + * Filter for class path entries. + */ + private static final class ClassPathEntryFilter { + + private final List exclusions; + + private final AntPathMatcher matcher = new AntPathMatcher(); + + private ClassPathEntryFilter(List annotations) { + Set exclusions = new LinkedHashSet<>(); + for (MergedAnnotations candidate : annotations) { + MergedAnnotation annotation = candidate.get(ClassPathExclusions.class); + if (annotation.isPresent()) { + exclusions.addAll(Arrays.asList(annotation.getStringArray(MergedAnnotation.VALUE))); + } + } + this.exclusions = exclusions.stream().toList(); + } + + private boolean isExcluded(URL url) { + if ("file".equals(url.getProtocol())) { + try { + String name = new File(url.toURI()).getName(); + for (String exclusion : this.exclusions) { + if (this.matcher.match(exclusion, name)) { + return true; + } + } + } + catch (URISyntaxException ex) { + } + } + return false; + } + + } + +} diff --git a/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathExtension.java b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathExtension.java new file mode 100644 index 00000000..cbd6f7f7 --- /dev/null +++ b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathExtension.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-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.cloud.test; + +import java.lang.reflect.Method; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.extension.Extension; +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.util.CollectionUtils; + +/** + * A custom {@link Extension} that runs tests using a modified class path. Entries are + * excluded from the class path using {@link ClassPathExclusions @ClassPathExclusions} and + * overridden using {@link ClassPathOverrides @ClassPathOverrides} on the test class. + * A class loader is created with the customized class path and is used both to load + * the test class and as the thread context class loader while the test is being run. + * + * @author Christoph Dreis + * @author Siva Krishna Battu + * @see ModifiedClassPathExtension.java + */ +class ModifiedClassPathExtension implements InvocationInterceptor { + + @Override + public void interceptBeforeAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptBeforeEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptAfterEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptAfterAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + interceptMethod(invocation, invocationContext, extensionContext); + } + + @Override + public void interceptTestTemplateMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + interceptMethod(invocation, invocationContext, extensionContext); + } + + private void interceptMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + if (isModifiedClassPathClassLoader(extensionContext)) { + invocation.proceed(); + return; + } + Class testClass = extensionContext.getRequiredTestClass(); + 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(); + Thread.currentThread().setContextClassLoader(modifiedClassLoader); + try { + runTest(extensionContext.getUniqueId()); + } + finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + + private void runTest(String testId) throws Throwable { + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectUniqueId(testId)).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 void intercept(Invocation invocation, ExtensionContext extensionContext) throws Throwable { + if (isModifiedClassPathClassLoader(extensionContext)) { + invocation.proceed(); + return; + } + invocation.skip(); + } + + private boolean isModifiedClassPathClassLoader(ExtensionContext extensionContext) { + Class testClass = extensionContext.getRequiredTestClass(); + ClassLoader classLoader = testClass.getClassLoader(); + return classLoader.getClass().getName().equals(ModifiedClassPathClassLoader.class.getName()); + } + +} diff --git a/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathRunner.java b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathRunner.java index 8d712637..de4c237b 100644 --- a/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathRunner.java +++ b/spring-cloud-test-support/src/main/java/org/springframework/cloud/test/ModifiedClassPathRunner.java @@ -59,6 +59,7 @@ import org.springframework.util.AntPathMatcher; import org.springframework.util.StringUtils; /** + * * A custom {@link BlockJUnit4ClassRunner} that runs tests using a modified class path. * Entries are excluded from the class path using * {@link ClassPathExclusions @ClassPathExclusions} and overridden using @@ -67,7 +68,10 @@ import org.springframework.util.StringUtils; * the thread context class loader while the test is being run. * * @author Andy Wilkinson + * @deprecated Replaced by {@link ModifiedClassPathExtension} */ + +@Deprecated public class ModifiedClassPathRunner extends BlockJUnit4ClassRunner { private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar"); diff --git a/spring-cloud-test-support/src/test/java/org/springframework/cloud/test/ModifiedClassPathRunnerExclusionsTests.java b/spring-cloud-test-support/src/test/java/org/springframework/cloud/test/ModifiedClassPathRunnerExclusionsTests.java index 8d068193..e35c996c 100644 --- a/spring-cloud-test-support/src/test/java/org/springframework/cloud/test/ModifiedClassPathRunnerExclusionsTests.java +++ b/spring-cloud-test-support/src/test/java/org/springframework/cloud/test/ModifiedClassPathRunnerExclusionsTests.java @@ -17,18 +17,16 @@ package org.springframework.cloud.test; import org.hamcrest.Matcher; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.isA; /** - * Tests for {@link ModifiedClassPathRunner} excluding entries from the class path. + * Tests for {@link ModifiedClassPathExtension} excluding entries from the class path. * * @author Andy Wilkinson */ -@RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions("hibernate-validator-*.jar") public class ModifiedClassPathRunnerExclusionsTests { diff --git a/spring-cloud-test-support/src/test/java/org/springframework/cloud/test/ModifiedClassPathRunnerOverridesTests.java b/spring-cloud-test-support/src/test/java/org/springframework/cloud/test/ModifiedClassPathRunnerOverridesTests.java index 5c11265f..6b6ccd30 100644 --- a/spring-cloud-test-support/src/test/java/org/springframework/cloud/test/ModifiedClassPathRunnerOverridesTests.java +++ b/spring-cloud-test-support/src/test/java/org/springframework/cloud/test/ModifiedClassPathRunnerOverridesTests.java @@ -16,8 +16,7 @@ package org.springframework.cloud.test; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.util.StringUtils; @@ -25,11 +24,10 @@ import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link ModifiedClassPathRunner} overriding entries on the class path. + * Tests for {@link ModifiedClassPathExtension} overriding entries on the class path. * * @author Andy Wilkinson */ -@RunWith(ModifiedClassPathRunner.class) @ClassPathOverrides("org.springframework:spring-context:4.1.0.RELEASE") public class ModifiedClassPathRunnerOverridesTests {