From 3acea583ad0f97b2cfa3dc463668ec1488ef254c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 24 Feb 2025 10:22:58 +0000 Subject: [PATCH] Add support for working with resources in tests Closes gh-44444 --- .../classpath/resources/ResourceContent.java | 44 ++++ .../classpath/resources/ResourcePath.java | 48 +++++ .../classpath/resources/Resources.java | 130 ++++++++++++ .../resources/ResourcesClassLoader.java | 62 ++++++ .../resources/ResourcesExtension.java | 189 ++++++++++++++++++ .../classpath/resources/ResourcesRoot.java | 41 ++++ .../resources/WithPackageResources.java | 45 +++++ .../classpath/resources/WithResource.java | 57 ++++++ .../resources/WithResourceDirectories.java | 39 ++++ .../resources/WithResourceDirectory.java | 47 +++++ .../classpath/resources/WithResources.java | 39 ++++ .../classpath/resources/package-info.java | 20 ++ .../OnClassWithPackageResourcesTests.java | 44 ++++ .../resources/OnClassWithResourceTests.java | 89 +++++++++ ...OnSuperClassWithPackageResourcesTests.java | 43 ++++ .../OnSuperClassWithResourceTests.java | 62 ++++++ .../classpath/resources/ResourcesTests.java | 132 ++++++++++++ .../resources/WithPackageResourcesClass.java | 27 +++ .../resources/WithPackageResourcesTests.java | 53 +++++ .../resources/WithResourceClass.java | 27 +++ .../resources/WithResourceDirectoryTests.java | 56 ++++++ .../resources/WithResourceTests.java | 100 +++++++++ .../classpath/resources/resource-1.txt | 1 + .../classpath/resources/resource-2.txt | 1 + .../classpath/resources/sub/resource-3.txt | 1 + 25 files changed, 1397 insertions(+) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourceContent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcePath.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/Resources.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesClassLoader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesExtension.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesRoot.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResources.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResource.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectories.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectory.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResources.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnClassWithPackageResourcesTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnClassWithResourceTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnSuperClassWithPackageResourcesTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnSuperClassWithResourceTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/ResourcesTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResourcesClass.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResourcesTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceClass.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectoryTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/resource-1.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/resource-2.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/sub/resource-3.txt diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourceContent.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourceContent.java new file mode 100644 index 00000000000..85a81e3aa39 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourceContent.java @@ -0,0 +1,44 @@ +/* + * 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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that indicates that the content of a resource should be injected. Supported + * on parameters of type: + * + * + * + * @author Andy Wilkinson + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ResourceContent { + + /** + * The name of the resource whose content should be injected. + * @return the resource name + */ + String value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcePath.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcePath.java new file mode 100644 index 00000000000..95644acee2f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcePath.java @@ -0,0 +1,48 @@ +/* + * 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.io.File; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.nio.file.Path; + +/** + * Annotation that indicates that the path of a resource should be injected. Supported on + * parameters of type: + * + * + * + * @author Andy Wilkinson + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ResourcePath { + + /** + * The name of the resource whose path should be injected. + * @return the resource name + */ + String value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/Resources.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/Resources.java new file mode 100644 index 00000000000..331b2b64059 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/Resources.java @@ -0,0 +1,130 @@ +/* + * 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.io.IOException; +import java.io.UncheckedIOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.util.Assert; +import org.springframework.util.FileSystemUtils; + +/** + * A collection of resources. + * + * @author Andy Wilkinson + */ +class Resources { + + private final Path root; + + Resources(Path root) { + this.root = root; + } + + Resources addPackage(Package root, String[] resourceNames) { + Set unmatchedNames = new HashSet<>(Arrays.asList(resourceNames)); + try { + Enumeration sources = getClass().getClassLoader().getResources(root.getName().replace(".", "/")); + for (URL source : Collections.list(sources)) { + Path sourceRoot = Paths.get(source.toURI()); + for (String resourceName : resourceNames) { + Path resource = sourceRoot.resolve(resourceName); + if (Files.isRegularFile(resource)) { + Path target = this.root.resolve(resourceName); + Path targetDirectory = target.getParent(); + if (!Files.isDirectory(targetDirectory)) { + Files.createDirectories(targetDirectory); + } + Files.copy(resource, target); + unmatchedNames.remove(resourceName); + } + } + } + Assert.isTrue(unmatchedNames.isEmpty(), + "Package '" + root.getName() + "' did not contain resources: " + unmatchedNames); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + catch (URISyntaxException ex) { + throw new RuntimeException(ex); + } + return this; + } + + Resources addResource(String name, String content) { + Path resourcePath = this.root.resolve(name); + if (Files.isDirectory(resourcePath)) { + throw new IllegalStateException( + "Cannot create resource '" + name + "' as a directory already exists at that location"); + } + Path parent = resourcePath.getParent(); + try { + if (!Files.isDirectory(resourcePath)) { + Files.createDirectories(parent); + } + Files.writeString(resourcePath, processContent(content)); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + return this; + } + + private String processContent(String content) { + return content.replace("${resourceRoot}", this.root.toString()); + } + + Resources addDirectory(String name) { + Path directoryPath = this.root.resolve(name); + if (Files.isRegularFile(directoryPath)) { + throw new IllegalStateException( + "Cannot create directory '" + name + " as a file already exists at that location"); + } + try { + Files.createDirectories(directoryPath); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + return this; + } + + void delete() { + try { + FileSystemUtils.deleteRecursively(this.root); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + Path getRoot() { + return this.root; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesClassLoader.java new file mode 100644 index 00000000000..1459f60680a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesClassLoader.java @@ -0,0 +1,62 @@ +/* + * 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.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +/** + * A {@link ClassLoader} that provides access to {@link Resources resources}. + * + * @author Andy Wilkinson + */ +class ResourcesClassLoader extends ClassLoader { + + private final Resources resources; + + ResourcesClassLoader(ClassLoader parent, Resources resources) { + super(parent); + this.resources = resources; + } + + @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); + } + } + return null; + } + + @Override + protected Enumeration findResources(String name) throws IOException { + URL resourceUrl = findResource(name); + return (resourceUrl != null) ? Collections.enumeration(List.of(resourceUrl)) : Collections.emptyEnumeration(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesExtension.java new file mode 100644 index 00000000000..72b75d24e36 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesExtension.java @@ -0,0 +1,189 @@ +/* + * 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.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.commons.support.SearchOption; + +import org.springframework.util.StreamUtils; + +/** + * {@link Extension} for managing resources in tests. Resources are made available through + * {@link Thread#getContextClassLoader() thread context class loader}. + * + * @author Andy Wilkinson + * @see WithPackageResources + * @see WithResource + * @see WithResourceDirectory + */ +class ResourcesExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver { + + private static final String RESOURCES_KEY = ResourcesExtension.class.getName() + ".resources"; + + private static final String TCCL_KEY = ResourcesExtension.class.getName() + ".tccl"; + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + Store store = context.getStore(Namespace.create(ResourcesExtension.class)); + 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())); + resourceDirectoriesOf(testMethod).forEach((directory) -> resources.addDirectory(directory.value())); + packageResourcesOf(testMethod).forEach((withPackageResources) -> resources + .addPackage(testMethod.getDeclaringClass().getPackage(), withPackageResources.value())); + ResourcesClassLoader classLoader = new ResourcesClassLoader(context.getRequiredTestClass().getClassLoader(), + resources); + store.put(TCCL_KEY, Thread.currentThread().getContextClassLoader()); + Thread.currentThread().setContextClassLoader(classLoader); + } + + private List resourcesOf(Method method) { + return withAnnotationsOf(method, WithResource.class); + } + + private List resourceDirectoriesOf(Method method) { + return withAnnotationsOf(method, WithResourceDirectory.class); + } + + private List withAnnotationsOf(Method method, Class annotationType) { + List annotations = new ArrayList<>(); + AnnotationSupport.findRepeatableAnnotations(method, annotationType).forEach(annotations::add); + Class type = method.getDeclaringClass(); + while (type != null) { + AnnotationSupport.findRepeatableAnnotations(type, annotationType).forEach(annotations::add); + type = type.getEnclosingClass(); + } + return annotations; + } + + private List packageResourcesOf(Method method) { + List annotations = new ArrayList<>(); + AnnotationSupport.findAnnotation(method, WithPackageResources.class).ifPresent(annotations::add); + AnnotationSupport + .findAnnotation(method.getDeclaringClass(), WithPackageResources.class, + SearchOption.INCLUDE_ENCLOSING_CLASSES) + .ifPresent(annotations::add); + return annotations; + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + Store store = context.getStore(Namespace.create(ResourcesExtension.class)); + store.get(RESOURCES_KEY, Resources.class).delete(); + Thread.currentThread().setContextClassLoader(store.get(TCCL_KEY, ClassLoader.class)); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.isAnnotated(ResourcesRoot.class) || parameterContext.isAnnotated(ResourcePath.class) + || parameterContext.isAnnotated(ResourceContent.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + if (parameterContext.isAnnotated(ResourcesRoot.class)) { + return resolveResourcesRoot(parameterContext, extensionContext); + } + if (parameterContext.isAnnotated(ResourcePath.class)) { + return resolveResourcePath(parameterContext, extensionContext); + } + if (parameterContext.isAnnotated(ResourceContent.class)) { + return resolveResourceContent(parameterContext, extensionContext); + } + throw new ParameterResolutionException( + "Parameter is not annotated with @ResourcesRoot, @ResourceContent, or @ResourcePath"); + } + + private Object resolveResourcesRoot(ParameterContext parameterContext, ExtensionContext extensionContext) { + Resources resources = getResources(extensionContext); + Class parameterType = parameterContext.getParameter().getType(); + if (parameterType.isAssignableFrom(Path.class)) { + return resources.getRoot(); + } + else if (parameterType.isAssignableFrom(File.class)) { + return resources.getRoot().toFile(); + } + throw new IllegalStateException( + "@ResourcesRoot is not supported with parameter type '" + parameterType.getName() + "'"); + } + + 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()); + if (parameterType.isAssignableFrom(Path.class)) { + return resourcePath; + } + else if (parameterType.isAssignableFrom(File.class)) { + return resourcePath.toFile(); + } + else if (parameterType.isAssignableFrom(String.class)) { + return resourcePath.toString(); + } + throw new IllegalStateException( + "@ResourcePath is not supported with parameter type '" + parameterType.getName() + "'"); + } + + 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()); + if (parameterType.isAssignableFrom(String.class)) { + try (InputStream in = Files.newInputStream(resourcePath)) { + return StreamUtils.copyToString(in, StandardCharsets.UTF_8); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + throw new IllegalStateException( + "@ResourceContent is not supported with parameter type '" + parameterType.getName() + "'"); + } + + private Resources getResources(ExtensionContext extensionContext) { + Store store = extensionContext.getStore(Namespace.create(ResourcesExtension.class)); + Resources resources = store.get(RESOURCES_KEY, Resources.class); + return resources; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesRoot.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesRoot.java new file mode 100644 index 00000000000..f3d7123c30d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesRoot.java @@ -0,0 +1,41 @@ +/* + * 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.io.File; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.nio.file.Path; + +/** + * Annotation that indicates that the resources root should be injected. Supported on + * parameters of type: + * + *
    + *
  • {@link File}
  • + *
  • {@link Path}
  • + *
+ * + * @author Andy Wilkinson + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ResourcesRoot { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResources.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResources.java new file mode 100644 index 00000000000..074d05bdf7e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResources.java @@ -0,0 +1,45 @@ +/* + * 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.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Makes resources in the package of the annotated class available from the root of the + * classpath. + * + * @author Andy Wilkinson + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +@ExtendWith(ResourcesExtension.class) +public @interface WithPackageResources { + + /** + * The resources to make available from the root. + * @return the resources + */ + String[] value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResource.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResource.java new file mode 100644 index 00000000000..bb573bb666c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResource.java @@ -0,0 +1,57 @@ +/* + * 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.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Makes a resource directory available from the thread context class loader. + *

+ * For cases where one resource needs to refer to another, the resource's content may + * contain the placeholder ${resourceRoot}. It will be replaced with the path + * to the root of the resources. For example, a resource with the {@link #name} + * {@code example.txt} can be referenced using ${resourceRoot}/example.txt. + * + * @author Andy Wilkinson + */ +@Inherited +@Repeatable(WithResources.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +@ExtendWith(ResourcesExtension.class) +public @interface WithResource { + + /** + * The name of the resource. + * @return the name + */ + String name(); + + /** + * The content of the resource. When omitted an empty resource will be created. + * @return the content + */ + String content() default ""; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectories.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectories.java new file mode 100644 index 00000000000..973931c65c2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectories.java @@ -0,0 +1,39 @@ +/* + * 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation for {@link WithResourceDirectory}. + * + * @author Andy Wilkinson + */ +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface WithResourceDirectories { + + WithResourceDirectory[] value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectory.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectory.java new file mode 100644 index 00000000000..95fb2ef7585 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectory.java @@ -0,0 +1,47 @@ +/* + * 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.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Makes a resource available from the thread context class loader. Typically used when a + * test requires an empty directory to exist. + * + * @author Andy Wilkinson + */ +@Inherited +@Repeatable(WithResourceDirectories.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +@ExtendWith(ResourcesExtension.class) +public @interface WithResourceDirectory { + + /** + * The name of the directory. + * @return the name + */ + String value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResources.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResources.java new file mode 100644 index 00000000000..1f9b72e767b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResources.java @@ -0,0 +1,39 @@ +/* + * 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation for {@link WithResource}. + * + * @author Andy Wilkinson + */ +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface WithResources { + + WithResource[] value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/package-info.java new file mode 100644 index 00000000000..ddc2e5c98ec --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Custom JUnit extension for testing with resources. + */ +package org.springframework.boot.testsupport.classpath.resources; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnClassWithPackageResourcesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnClassWithPackageResourcesTests.java new file mode 100644 index 00000000000..31949514396 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnClassWithPackageResourcesTests.java @@ -0,0 +1,44 @@ +/* + * 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.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WithPackageResources} on a class. + * + * @author Andy Wilkinson + */ +@WithPackageResources({ "resource-1.txt", "resource-2.txt", "sub/resource-3.txt" }) +class OnClassWithPackageResourcesTests { + + @Test + void whenWithPackageResourcesIsUsedOnAClassThenResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("resource-1.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("one"); + assertThat(new ClassPathResource("resource-2.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("two"); + assertThat(new ClassPathResource("sub/resource-3.txt").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("three"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnClassWithResourceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnClassWithResourceTests.java new file mode 100644 index 00000000000..a1c701e166c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnClassWithResourceTests.java @@ -0,0 +1,89 @@ +/* + * 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.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WithResource} when used on a class. + * + * @author Andy Wilkinson + */ +@WithResource(name = "on-class", content = "class content") +class OnClassWithResourceTests { + + @Test + void whenWithResourceIsUsedOnAClassThenResourceIsAvailable() throws IOException { + assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("class content"); + } + + @Test + @WithResource(name = "method-resource", content = "method") + void whenWithResourceIsUsedOnClassAndMethodThenBothResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("class content"); + assertThat(new ClassPathResource("method-resource").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("method"); + } + + @Test + @WithResource(name = "method-resource-1", content = "method-1") + @WithResource(name = "method-resource-2", content = "method-2") + void whenWithResourceIsUsedOnClassAndRepeatedOnMethodThenAllResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("class content"); + assertThat(new ClassPathResource("method-resource-1").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("method-1"); + assertThat(new ClassPathResource("method-resource-2").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("method-2"); + } + + @Nested + class NestedTests { + + @Test + void whenWithResourceIsUsedOnEnclosingClassThenResourceIsAvailable() throws IOException { + assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("class content"); + } + + } + + @Nested + @WithResource(name = "on-nested-class", content = "nested class content") + class WithResourceNestedTests { + + @Test + void whenWithResourceIsUsedOnEnclosingClassAndClassThenBothResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("class content"); + assertThat(new ClassPathResource("on-nested-class").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("nested class content"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnSuperClassWithPackageResourcesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnSuperClassWithPackageResourcesTests.java new file mode 100644 index 00000000000..2a3b3739a09 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnSuperClassWithPackageResourcesTests.java @@ -0,0 +1,43 @@ +/* + * 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.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WithPackageResources} when used on a super-class. + * + * @author Andy Wilkinson + */ +class OnSuperClassWithPackageResourcesTests extends WithPackageResourcesClass { + + @Test + void whenWithPackageResourcesIsUsedOnASuperClassThenResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("resource-1.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("one"); + assertThat(new ClassPathResource("resource-2.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("two"); + assertThat(new ClassPathResource("sub/resource-3.txt").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("three"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnSuperClassWithResourceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnSuperClassWithResourceTests.java new file mode 100644 index 00000000000..2e47735fb14 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/OnSuperClassWithResourceTests.java @@ -0,0 +1,62 @@ +/* + * 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.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WithResource} when used on a super-class. + * + * @author Andy Wilkinson + */ +class OnSuperClassWithResourceTests extends WithResourceClass { + + @Test + void whenWithResourceIsUsedOnASuperClassThenResourceIsAvailable() throws IOException { + assertThat(new ClassPathResource("on-super-class").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("super-class content"); + } + + @Test + @WithResource(name = "method-resource", content = "method") + void whenWithResourceIsUsedOnASuperClassAndMethodThenBothResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("on-super-class").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("super-class content"); + assertThat(new ClassPathResource("method-resource").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("method"); + } + + @Test + @WithResource(name = "method-resource-1", content = "method-1") + @WithResource(name = "method-resource-2", content = "method-2") + void whenWithResourceIsUsedOnASuperClassAndRepeatedOnMethodThenAllResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("on-super-class").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("super-class content"); + assertThat(new ClassPathResource("method-resource-1").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("method-1"); + assertThat(new ClassPathResource("method-resource-2").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("method-2"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/ResourcesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/ResourcesTests.java new file mode 100644 index 00000000000..c91d508c68e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/ResourcesTests.java @@ -0,0 +1,132 @@ +/* + * 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; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link Resources}. + * + * @author Andy Wilkinson + */ +class ResourcesTests { + + @TempDir + private Path root; + + @Test + void whenAddResourceThenResourceIsCreated() { + new Resources(this.root).addResource("test", "test-content"); + assertThat(this.root.resolve("test")).hasContent("test-content"); + } + + @Test + void whenAddResourceHasContentReferencingResourceRootThenResourceIsCreatedWithReferenceToRoot() { + new Resources(this.root).addResource("test", "*** ${resourceRoot} ***"); + assertThat(this.root.resolve("test")).hasContent("*** " + this.root + " ***"); + } + + @Test + void whenAddResourceWithPathThenResourceIsCreated() { + new Resources(this.root).addResource("a/b/c/test", "test-content"); + assertThat(this.root.resolve("a/b/c/test")).hasContent("test-content"); + } + + @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"); + 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" }); + 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"); + } + + @Test + void whenAddResourceAndDeleteThenResourceDoesNotExist() { + Resources resources = new Resources(this.root); + resources.addResource("test", "test-content"); + assertThat(this.root.resolve("test")).hasContent("test-content"); + resources.delete(); + assertThat(this.root.resolve("test")).doesNotExist(); + } + + @Test + void whenAddPackageAndDeleteThenResourcesDoNotExist() { + Resources resources = new Resources(this.root); + 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.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(); + } + + @Test + void whenAddDirectoryThenDirectoryIsCreated() { + Resources resources = new Resources(this.root); + resources.addDirectory("dir"); + assertThat(this.root.resolve("dir")).isDirectory(); + } + + @Test + void whenAddDirectoryWithPathThenDirectoryIsCreated() { + Resources resources = new Resources(this.root); + resources.addDirectory("one/two/three/dir"); + assertThat(this.root.resolve("one/two/three/dir")).isDirectory(); + } + + @Test + void whenAddDirectoryAndDirectoryAlreadyExistsThenDoesNotThrow() { + Resources resources = new Resources(this.root); + resources.addDirectory("one/two/three/dir"); + 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")); + } + + @Test + void whenAddResourceAndDirectoryAlreadyExistsThenIllegalStateExceptionIsThrown() { + Resources resources = new Resources(this.root); + resources.addDirectory("one/two/three"); + assertThatIllegalStateException().isThrownBy(() -> resources.addResource("one/two/three", "content")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResourcesClass.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResourcesClass.java new file mode 100644 index 00000000000..754e48c8fb7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResourcesClass.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * Class to test the use of {@link WithPackageResources} on a super-class. + * + * @author Andy Wilkinson + */ +@WithPackageResources({ "resource-1.txt", "resource-2.txt", "sub/resource-3.txt" }) +class WithPackageResourcesClass { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResourcesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResourcesTests.java new file mode 100644 index 00000000000..8d5f425b30c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithPackageResourcesTests.java @@ -0,0 +1,53 @@ +/* + * 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.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WithPackageResources}. + * + * @author Andy Wilkinson + */ +class WithPackageResourcesTests { + + @Test + @WithPackageResources({ "resource-1.txt", "resource-2.txt", "sub/resource-3.txt" }) + void whenWithPackageResourcesIsUsedOnAMethodThenResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("resource-1.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("one"); + assertThat(new ClassPathResource("resource-2.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("two"); + assertThat(new ClassPathResource("sub/resource-3.txt").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("three"); + } + + @Test + @WithPackageResources("sub/resource-3.txt") + void whenWithPackageResourcesOnlyIncludesSomeResourcesThenOnlyIncludedResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("resource-1.txt").exists()).isFalse(); + assertThat(new ClassPathResource("resource-2.txt").exists()).isFalse(); + assertThat(new ClassPathResource("sub/resource-3.txt").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("three"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceClass.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceClass.java new file mode 100644 index 00000000000..0b386bbd5fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceClass.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * Class to test the use of {@link WithResource} on a super-class + * + * @author Andy Wilkinson + */ +@WithResource(name = "on-super-class", content = "super-class content") +class WithResourceClass { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectoryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectoryTests.java new file mode 100644 index 00000000000..7e3d29a4de6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceDirectoryTests.java @@ -0,0 +1,56 @@ +/* + * 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.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WithResourceDirectory}. + * + * @author Andy Wilkinson + */ +class WithResourceDirectoryTests { + + @Test + @WithResourceDirectory("test") + void whenWithResourceDirectoryIsUsedOnAMethodThenDirectoryIsCreated() throws IOException { + assertThat(new ClassPathResource("test").getFile()).isDirectory(); + } + + @Test + @WithResourceDirectory("com/example/nested") + void whenWithResourceDirectoryNamesANestedDirectoryThenDirectoryIsCreated() throws IOException { + assertThat(new ClassPathResource("com/example/nested").getFile()).isDirectory(); + } + + @Test + @WithResourceDirectory("1") + @WithResourceDirectory("2") + @WithResourceDirectory("3") + void whenWithResourceDirectoryIsRepeatedOnAMethodThenAllResourceDirectoriesAreCreated() throws IOException { + assertThat(new ClassPathResource("1").getFile()).isDirectory(); + assertThat(new ClassPathResource("2").getFile()).isDirectory(); + assertThat(new ClassPathResource("3").getFile()).isDirectory(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceTests.java new file mode 100644 index 00000000000..87e15b16c41 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/WithResourceTests.java @@ -0,0 +1,100 @@ +/* + * 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.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WithResource}. + * + * @author Andy Wilkinson + */ +class WithResourceTests { + + @Test + @WithResource(name = "test", content = "content") + void whenWithResourceIsUsedOnAMethodThenResourceIsAvailable() throws IOException { + assertThat(new ClassPathResource("test").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("content"); + } + + @Test + @WithResource(name = "test", content = "content") + void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromFileResourcesRoot(@ResourcesRoot File root) { + assertThat(new File(root, "test")).hasContent("content"); + } + + @Test + @WithResource(name = "test", content = "content") + void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromPathResourcesRoot(@ResourcesRoot Path root) { + assertThat(root.resolve("test")).hasContent("content"); + } + + @Test + @WithResource(name = "test", content = "content") + void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromPathResourcePath( + @ResourcePath("test") Path resource) { + assertThat(resource).hasContent("content"); + } + + @Test + @WithResource(name = "test", content = "content") + void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromFileResourcePath( + @ResourcePath("test") File resource) { + assertThat(resource).hasContent("content"); + } + + @Test + @WithResource(name = "test", content = "content") + void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromStringResourcePath( + @ResourcePath("test") String resource) { + assertThat(new File(resource)).hasContent("content"); + } + + @Test + @WithResource(name = "test", content = "content") + void whenWithResourceIsUsedOnAMethodThenResourceContentIsAvailableAsAString( + @ResourceContent("test") String content) { + assertThat(content).isEqualTo("content"); + } + + @Test + @WithResource(name = "com/example/test-resource", content = "content") + void whenWithResourceNameIncludesADirectoryThenResourceIsAvailable() throws IOException { + assertThat(new ClassPathResource("com/example/test-resource").getContentAsString(StandardCharsets.UTF_8)) + .isEqualTo("content"); + } + + @Test + @WithResource(name = "1", content = "one") + @WithResource(name = "2", content = "two") + @WithResource(name = "3", content = "three") + void whenWithResourceIsRepeatedOnAMethodThenAllResourcesAreAvailable() throws IOException { + assertThat(new ClassPathResource("1").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("one"); + assertThat(new ClassPathResource("2").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("two"); + assertThat(new ClassPathResource("3").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("three"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/resource-1.txt b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/resource-1.txt new file mode 100644 index 00000000000..43dd47ea691 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/resource-1.txt @@ -0,0 +1 @@ +one \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/resource-2.txt b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/resource-2.txt new file mode 100644 index 00000000000..64c5e5885a4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/resource-2.txt @@ -0,0 +1 @@ +two \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/sub/resource-3.txt b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/sub/resource-3.txt new file mode 100644 index 00000000000..1d19714ffbc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/resources/org/springframework/boot/testsupport/classpath/resources/sub/resource-3.txt @@ -0,0 +1 @@ +three \ No newline at end of file