Merge branch '3.3.x' into 3.4.x

This commit is contained in:
Andy Wilkinson 2025-03-05 14:34:11 +00:00
commit bfd4e7b4ee
7 changed files with 183 additions and 56 deletions

View File

@ -0,0 +1,31 @@
/*
* Copyright 2012-2025 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.resources;
import java.nio.file.Path;
/**
* A resource that is to be made available in tests.
*
* @param path the path of the resoure
* @param additional whether the resource should be made available in addition to those
* that already exist elsewhere
* @author Andy Wilkinson
*/
record Resource(Path path, boolean additional) {
}

View File

@ -26,7 +26,9 @@ import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.util.Assert;
@ -41,6 +43,8 @@ class Resources {
private final Path root;
private final Map<String, Resource> resources = new HashMap<>();
Resources(Path root) {
this.root = root;
}
@ -60,6 +64,7 @@ class Resources {
Files.createDirectories(targetDirectory);
}
Files.copy(resource, target);
register(resourceName, target, true);
unmatchedNames.remove(resourceName);
}
}
@ -76,7 +81,7 @@ class Resources {
return this;
}
Resources addResource(String name, String content) {
Resources addResource(String name, String content, boolean additional) {
Path resourcePath = this.root.resolve(name);
if (Files.isDirectory(resourcePath)) {
throw new IllegalStateException(
@ -88,6 +93,7 @@ class Resources {
Files.createDirectories(parent);
}
Files.writeString(resourcePath, processContent(content));
register(name, resourcePath, additional);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
@ -95,6 +101,29 @@ class Resources {
return this;
}
private void register(String name, Path resourcePath, boolean additional) {
Resource resource = new Resource(resourcePath, additional);
register(name, resource);
Path ancestor = resourcePath.getParent();
while (!this.root.equals(ancestor)) {
Resource ancestorResource = new Resource(ancestor, additional);
register(this.root.relativize(ancestor).toString(), ancestorResource);
ancestor = ancestor.getParent();
}
}
private void register(String name, Resource resource) {
this.resources.put(name, resource);
if (Files.isDirectory(resource.path())) {
if (name.endsWith("/")) {
this.resources.put(name.substring(0, name.length() - 1), resource);
}
else {
this.resources.put(name + "/", resource);
}
}
}
private String processContent(String content) {
return content.replace("${resourceRoot}", this.root.toString());
}
@ -107,6 +136,7 @@ class Resources {
}
try {
Files.createDirectories(directoryPath);
register(name, directoryPath, true);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
@ -117,6 +147,7 @@ class Resources {
void delete() {
try {
FileSystemUtils.deleteRecursively(this.root);
this.resources.clear();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
@ -127,4 +158,8 @@ class Resources {
return this.root;
}
Resource find(String name) {
return this.resources.get(name);
}
}

View File

@ -19,11 +19,9 @@ package org.springframework.boot.testsupport.classpath.resources;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
/**
* A {@link ClassLoader} that provides access to {@link Resources resources}.
@ -40,23 +38,32 @@ class ResourcesClassLoader extends ClassLoader {
}
@Override
protected URL findResource(String name) {
Path resource = this.resources.getRoot().resolve(name);
if (Files.exists(resource)) {
try {
return resource.toUri().toURL();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
public URL getResource(String name) {
Resource resource = this.resources.find(name);
return (resource != null) ? urlOf(resource) : getParent().getResource(name);
}
private URL urlOf(Resource resource) {
try {
return resource.path().toUri().toURL();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return null;
}
@Override
protected Enumeration<URL> findResources(String name) throws IOException {
URL resourceUrl = findResource(name);
return (resourceUrl != null) ? Collections.enumeration(List.of(resourceUrl)) : Collections.emptyEnumeration();
public Enumeration<URL> getResources(String name) throws IOException {
Resource resource = this.resources.find(name);
ArrayList<URL> urls = new ArrayList<>();
if (resource != null) {
URL resourceUrl = urlOf(resource);
urls.add(resourceUrl);
}
if (resource == null || resource.additional()) {
urls.addAll(Collections.list(getParent().getResources(name)));
}
return Collections.enumeration(urls);
}
}

View File

@ -63,7 +63,8 @@ class ResourcesExtension implements BeforeEachCallback, AfterEachCallback, Param
Resources resources = new Resources(Files.createTempDirectory("resources"));
store.put(RESOURCES_KEY, resources);
Method testMethod = context.getRequiredTestMethod();
resourcesOf(testMethod).forEach((resource) -> resources.addResource(resource.name(), resource.content()));
resourcesOf(testMethod)
.forEach((resource) -> resources.addResource(resource.name(), resource.content(), resource.additional()));
resourceDirectoriesOf(testMethod).forEach((directory) -> resources.addDirectory(directory.value()));
packageResourcesOf(testMethod).forEach((withPackageResources) -> resources
.addPackage(testMethod.getDeclaringClass().getPackage(), withPackageResources.value()));
@ -148,8 +149,7 @@ class ResourcesExtension implements BeforeEachCallback, AfterEachCallback, Param
private Object resolveResourcePath(ParameterContext parameterContext, ExtensionContext extensionContext) {
Resources resources = getResources(extensionContext);
Class<?> parameterType = parameterContext.getParameter().getType();
Path resourcePath = resources.getRoot()
.resolve(parameterContext.findAnnotation(ResourcePath.class).get().value());
Path resourcePath = resources.find(parameterContext.findAnnotation(ResourcePath.class).get().value()).path();
if (parameterType.isAssignableFrom(Path.class)) {
return resourcePath;
}
@ -166,8 +166,7 @@ class ResourcesExtension implements BeforeEachCallback, AfterEachCallback, Param
private Object resolveResourceContent(ParameterContext parameterContext, ExtensionContext extensionContext) {
Resources resources = getResources(extensionContext);
Class<?> parameterType = parameterContext.getParameter().getType();
Path resourcePath = resources.getRoot()
.resolve(parameterContext.findAnnotation(ResourceContent.class).get().value());
Path resourcePath = resources.find(parameterContext.findAnnotation(ResourceContent.class).get().value()).path();
if (parameterType.isAssignableFrom(String.class)) {
try (InputStream in = Files.newInputStream(resourcePath)) {
return StreamUtils.copyToString(in, StandardCharsets.UTF_8);

View File

@ -54,4 +54,11 @@ public @interface WithResource {
*/
String content() default "";
/**
* Whether the resource should be available in addition to those that are already on
* the classpath are instead of any existing resources with the same name.
* @return whether this is an additional resource
*/
boolean additional() default true;
}

View File

@ -18,6 +18,7 @@ package org.springframework.boot.testsupport.classpath.resources;
import java.nio.file.Path;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@ -34,99 +35,122 @@ class ResourcesTests {
@TempDir
private Path root;
private Resources resources;
@BeforeEach
void setUp() {
this.resources = new Resources(this.root);
}
@Test
void whenAddResourceThenResourceIsCreated() {
new Resources(this.root).addResource("test", "test-content");
void whenAddResourceThenResourceIsCreatedAndCanBeFound() {
this.resources.addResource("test", "test-content", true);
assertThat(this.root.resolve("test")).hasContent("test-content");
assertThat(this.resources.find("test")).isNotNull();
}
@Test
void whenAddResourceHasContentReferencingResourceRootThenResourceIsCreatedWithReferenceToRoot() {
new Resources(this.root).addResource("test", "*** ${resourceRoot} ***");
this.resources.addResource("test", "*** ${resourceRoot} ***", true);
assertThat(this.root.resolve("test")).hasContent("*** " + this.root + " ***");
}
@Test
void whenAddResourceWithPathThenResourceIsCreated() {
new Resources(this.root).addResource("a/b/c/test", "test-content");
void whenAddResourceWithPathThenResourceIsCreatedAndItAndItsAncestorsCanBeFound() {
this.resources.addResource("a/b/c/test", "test-content", true);
assertThat(this.root.resolve("a/b/c/test")).hasContent("test-content");
assertThat(this.resources.find("a/b/c/test")).isNotNull();
assertThat(this.resources.find("a/b/c/")).isNotNull();
assertThat(this.resources.find("a/b/")).isNotNull();
assertThat(this.resources.find("a/")).isNotNull();
}
@Test
void whenAddResourceAndResourceAlreadyExistsThenResourcesIsOverwritten() {
Resources resources = new Resources(this.root);
resources.addResource("a/b/c/test", "original-content");
resources.addResource("a/b/c/test", "new-content");
this.resources.addResource("a/b/c/test", "original-content", true);
this.resources.addResource("a/b/c/test", "new-content", true);
assertThat(this.root.resolve("a/b/c/test")).hasContent("new-content");
}
@Test
void whenAddPackageThenNamedResourcesFromPackageAreCreated() {
new Resources(this.root).addPackage(getClass().getPackage(),
new String[] { "resource-1.txt", "sub/resource-3.txt" });
void whenAddPackageThenNamedResourcesFromPackageAreCreatedAndCanBeFound() {
this.resources.addPackage(getClass().getPackage(), new String[] { "resource-1.txt", "sub/resource-3.txt" });
assertThat(this.root.resolve("resource-1.txt")).hasContent("one");
assertThat(this.root.resolve("resource-2.txt")).doesNotExist();
assertThat(this.root.resolve("sub/resource-3.txt")).hasContent("three");
assertThat(this.resources.find("resource-1.txt")).isNotNull();
assertThat(this.resources.find("resource-2.txt")).isNull();
assertThat(this.resources.find("sub/resource-3.txt")).isNotNull();
assertThat(this.resources.find("sub/")).isNotNull();
}
@Test
void whenAddResourceAndDeleteThenResourceDoesNotExist() {
Resources resources = new Resources(this.root);
resources.addResource("test", "test-content");
void whenAddResourceAndDeleteThenResourceDoesNotExistAndCannotBeFound() {
this.resources.addResource("test", "test-content", true);
assertThat(this.root.resolve("test")).hasContent("test-content");
resources.delete();
assertThat(this.resources.find("test")).isNotNull();
this.resources.delete();
assertThat(this.root.resolve("test")).doesNotExist();
assertThat(this.resources.find("test")).isNull();
}
@Test
void whenAddPackageAndDeleteThenResourcesDoNotExist() {
Resources resources = new Resources(this.root);
resources.addPackage(getClass().getPackage(),
void whenAddPackageAndDeleteThenResourcesDoNotExistAndCannotBeFound() {
this.resources.addPackage(getClass().getPackage(),
new String[] { "resource-1.txt", "resource-2.txt", "sub/resource-3.txt" });
assertThat(this.root.resolve("resource-1.txt")).hasContent("one");
assertThat(this.root.resolve("resource-2.txt")).hasContent("two");
assertThat(this.root.resolve("sub/resource-3.txt")).hasContent("three");
resources.delete();
assertThat(this.resources.find("resource-1.txt")).isNotNull();
assertThat(this.resources.find("resource-2.txt")).isNotNull();
assertThat(this.resources.find("sub/resource-3.txt")).isNotNull();
assertThat(this.resources.find("sub/")).isNotNull();
this.resources.delete();
assertThat(this.root.resolve("resource-1.txt")).doesNotExist();
assertThat(this.root.resolve("resource-2.txt")).doesNotExist();
assertThat(this.root.resolve("sub/resource-3.txt")).doesNotExist();
assertThat(this.root.resolve("sub")).doesNotExist();
assertThat(this.resources.find("resource-1.txt")).isNull();
assertThat(this.resources.find("resource-2.txt")).isNull();
assertThat(this.resources.find("sub/resource-3.txt")).isNull();
assertThat(this.resources.find("sub/")).isNull();
}
@Test
void whenAddDirectoryThenDirectoryIsCreated() {
Resources resources = new Resources(this.root);
resources.addDirectory("dir");
void whenAddDirectoryThenDirectoryIsCreatedAndCanBeFound() {
this.resources.addDirectory("dir");
assertThat(this.root.resolve("dir")).isDirectory();
assertThat(this.resources.find("dir/")).isNotNull();
}
@Test
void whenAddDirectoryWithPathThenDirectoryIsCreated() {
Resources resources = new Resources(this.root);
resources.addDirectory("one/two/three/dir");
void whenAddDirectoryWithPathThenDirectoryIsCreatedAndItAndItsAncestorsCanBeFound() {
this.resources.addDirectory("one/two/three/dir");
assertThat(this.root.resolve("one/two/three/dir")).isDirectory();
assertThat(this.resources.find("one/two/three/dir/")).isNotNull();
assertThat(this.resources.find("one/two/three/")).isNotNull();
assertThat(this.resources.find("one/two/")).isNotNull();
assertThat(this.resources.find("one/")).isNotNull();
}
@Test
void whenAddDirectoryAndDirectoryAlreadyExistsThenDoesNotThrow() {
Resources resources = new Resources(this.root);
resources.addDirectory("one/two/three/dir");
resources.addDirectory("one/two/three/dir");
this.resources.addDirectory("one/two/three/dir");
this.resources.addDirectory("one/two/three/dir");
assertThat(this.root.resolve("one/two/three/dir")).isDirectory();
}
@Test
void whenAddDirectoryAndResourceAlreadyExistsThenIllegalStateExceptionIsThrown() {
Resources resources = new Resources(this.root);
resources.addResource("one/two/three/", "content");
assertThatIllegalStateException().isThrownBy(() -> resources.addDirectory("one/two/three"));
this.resources.addResource("one/two/three/", "content", true);
assertThatIllegalStateException().isThrownBy(() -> this.resources.addDirectory("one/two/three"));
}
@Test
void whenAddResourceAndDirectoryAlreadyExistsThenIllegalStateExceptionIsThrown() {
Resources resources = new Resources(this.root);
resources.addDirectory("one/two/three");
assertThatIllegalStateException().isThrownBy(() -> resources.addResource("one/two/three", "content"));
this.resources.addDirectory("one/two/three");
assertThatIllegalStateException()
.isThrownBy(() -> this.resources.addResource("one/two/three", "content", true));
}
}

View File

@ -24,6 +24,8 @@ import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import static org.assertj.core.api.Assertions.assertThat;
@ -97,4 +99,26 @@ class WithResourceTests {
assertThat(new ClassPathResource("3").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("three");
}
@Test
@WithResource(name = "org/springframework/boot/testsupport/classpath/resources/resource-1.txt",
content = "from-with-resource")
void whenWithResourceCreatesResourceThatIsAvailableElsewhereBothResourcesCanBeLoaded() throws IOException {
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("classpath*:org/springframework/boot/testsupport/classpath/resources/resource-1.txt");
assertThat(resources).hasSize(2);
assertThat(resources).extracting((resource) -> resource.getContentAsString(StandardCharsets.UTF_8))
.containsExactly("from-with-resource", "one");
}
@Test
@WithResource(name = "org/springframework/boot/testsupport/classpath/resources/resource-1.txt",
content = "from-with-resource", additional = false)
void whenWithResourceCreatesResourceThatIsNotAdditionalThenResourceThatIsAvailableElsewhereCannotBeLoaded()
throws IOException {
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("classpath*:org/springframework/boot/testsupport/classpath/resources/resource-1.txt");
assertThat(resources).hasSize(1);
assertThat(resources[0].getContentAsString(StandardCharsets.UTF_8)).isEqualTo("from-with-resource");
}
}