From 04cce0bafd21c3f6b8f427ab2eac75ffadefb585 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 4 Aug 2023 12:10:07 +0300 Subject: [PATCH] Support custom properties file formats in @TestPropertySource Spring Framework 4.3 introduced the `PropertySourceFactory` SPI for use with `@PropertySource` on `@Configuration` classes; however, prior to this commit there was no mechanism to support custom properties file formats in `@TestPropertySource` for integration tests. This commit introduces support for configuring a custom `PropertySourceFactory` via a new `factory` attribute in `@TestPropertySource` in order to support custom file formats such as JSON, YAML, etc. For example, if you create a YamlPropertySourceFactory, you can use it in integration tests as follows. @SpringJUnitConfig @TestPropertySource(locations = "/test.yaml", factory = YamlPropertySourceFactory.class) class MyTestClass { /* ... /* } If a custom factory is not specified, traditional `*.properties` and `*.xml` based `java.util.Properties` file formats are supported, which was the existing behavior. Closes gh-30981 --- .../ctx-management/property-sources.adoc | 13 +- spring-test/spring-test.gradle | 1 + .../context/MergedContextConfiguration.java | 102 +++++++++++++--- .../test/context/TestPropertySource.java | 27 +++-- ...ergedContextConfigurationRuntimeHints.java | 3 +- .../support/AbstractContextLoader.java | 6 +- .../AbstractTestContextBootstrapper.java | 2 +- .../support/MergedTestPropertySources.java | 36 +++--- .../support/TestPropertySourceAttributes.java | 85 +++++++++---- .../support/TestPropertySourceUtils.java | 114 +++++++++++++++--- .../web/WebMergedContextConfiguration.java | 60 ++++++++- .../MergedContextConfigurationTests.java | 9 +- .../env/YamlPropertySourceFactory.java | 62 ++++++++++ .../test/context/env/YamlTestProperties.java | 39 ++++++ .../env/YamlTestPropertySourceTests.java | 59 +++++++++ ...pertiesFileAndMetaPropertiesFileTests.java | 19 +-- ...calYamlFileAndMetaPropertiesFileTests.java | 43 +++++++ .../env/repeatable/MetaFileTestProperty.java | 34 ++++++ .../BootstrapTestUtilsMergedConfigTests.java | 3 +- .../support/TestPropertySourceUtilsTests.java | 7 +- ...AnnotationConfigWebContextLoaderTests.java | 9 +- .../web/GenericXmlWebContextLoaderTests.java | 7 +- .../test/context/env/repeatable/local.yaml | 5 + .../test/context/env/test-properties.yaml | 7 ++ 24 files changed, 622 insertions(+), 130 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/env/YamlPropertySourceFactory.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/env/YamlTestProperties.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/env/YamlTestPropertySourceTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalYamlFileAndMetaPropertiesFileTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/env/repeatable/MetaFileTestProperty.java create mode 100644 spring-test/src/test/resources/org/springframework/test/context/env/repeatable/local.yaml create mode 100644 spring-test/src/test/resources/org/springframework/test/context/env/test-properties.yaml diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc index 06c7f9625b..63243f041d 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc @@ -16,7 +16,7 @@ SPI, but `@TestPropertySource` is not supported with implementations of the olde `ContextLoader` SPI. Implementations of `SmartContextLoader` gain access to merged test property source values -through the `getPropertySourceLocations()` and `getPropertySourceProperties()` methods in +through the `getPropertySourceDescriptors()` and `getPropertySourceProperties()` methods in `MergedContextConfiguration`. ==== @@ -26,8 +26,11 @@ through the `getPropertySourceLocations()` and `getPropertySourceProperties()` m You can configure test properties files by using the `locations` or `value` attribute of `@TestPropertySource`. -Both traditional and XML-based properties file formats are supported -- for example, -`"classpath:/com/example/test.properties"` or `"file:///path/to/file.xml"`. +By default, both traditional and XML-based `java.util.Properties` file formats are +supported -- for example, `"classpath:/com/example/test.properties"` or +`"file:///path/to/file.xml"`. As of Spring Framework 6.1, you can configure a custom +`PropertySourceFactory` via the `factory` attribute in `@TestPropertySource` in order to +support a different file format such as JSON, YAML, etc. Each path is interpreted as a Spring `Resource`. A plain path (for example, `"test.properties"`) is treated as a classpath resource that is relative to the package @@ -35,8 +38,8 @@ in which the test class is defined. A path starting with a slash is treated as a absolute classpath resource (for example: `"/org/example/test.xml"`). A path that references a URL (for example, a path prefixed with `classpath:`, `file:`, or `http:`) is loaded by using the specified resource protocol. Resource location wildcards (such as -`**/*.properties`) are not permitted: Each location must evaluate to exactly one -`.properties` or `.xml` resource. +`{asterisk}{asterisk}/{asterisk}.properties`) are not permitted: Each location must +evaluate to exactly one properties resource. The following example uses a test properties file: diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index b1c3e9b071..01edf88137 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -85,6 +85,7 @@ dependencies { testRuntimeOnly("org.junit.vintage:junit-vintage-engine") { exclude group: "junit", module: "junit" } + testRuntimeOnly("org.yaml:snakeyaml") } // Prevent xml-apis from being used so that the corresponding XML APIs from diff --git a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java index 49d0181e25..8307b5eecd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -20,10 +20,12 @@ import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; +import org.springframework.core.io.support.PropertySourceDescriptor; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; @@ -91,6 +93,8 @@ public class MergedContextConfiguration implements Serializable { private final String[] activeProfiles; + private final List propertySourceDescriptors; + private final String[] propertySourceLocations; private final String[] propertySourceProperties; @@ -151,7 +155,7 @@ public class MergedContextConfiguration implements Serializable { * @param activeProfiles the merged active bean definition profiles * @param contextLoader the resolved {@code ContextLoader} * @param cacheAwareContextLoaderDelegate a cache-aware context loader - * delegate with which to retrieve the parent context + * delegate with which to retrieve the parent {@code ApplicationContext} * @param parent the parent configuration or {@code null} if there is no parent * @since 3.2.2 */ @@ -172,9 +176,10 @@ public class MergedContextConfiguration implements Serializable { */ public MergedContextConfiguration(MergedContextConfiguration mergedConfig) { this(mergedConfig.testClass, mergedConfig.locations, mergedConfig.classes, - mergedConfig.contextInitializerClasses, mergedConfig.activeProfiles, mergedConfig.propertySourceLocations, - mergedConfig.propertySourceProperties, mergedConfig.contextCustomizers, - mergedConfig.contextLoader, mergedConfig.cacheAwareContextLoaderDelegate, mergedConfig.parent); + mergedConfig.contextInitializerClasses, mergedConfig.activeProfiles, + mergedConfig.propertySourceDescriptors, mergedConfig.propertySourceProperties, + mergedConfig.contextCustomizers, mergedConfig.contextLoader, + mergedConfig.cacheAwareContextLoaderDelegate, mergedConfig.parent); } /** @@ -196,10 +201,13 @@ public class MergedContextConfiguration implements Serializable { * @param propertySourceProperties the merged {@code PropertySource} properties * @param contextLoader the resolved {@code ContextLoader} * @param cacheAwareContextLoaderDelegate a cache-aware context loader - * delegate with which to retrieve the parent context + * delegate with which to retrieve the parent {@code ApplicationContext} * @param parent the parent configuration or {@code null} if there is no parent * @since 4.1 + * @deprecated since 6.1 in favor of + * {@link #MergedContextConfiguration(Class, String[], Class[], Set, String[], List, String[], Set, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} */ + @Deprecated(since = "6.1") public MergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, @Nullable Set>> contextInitializerClasses, @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, @@ -233,10 +241,13 @@ public class MergedContextConfiguration implements Serializable { * @param contextCustomizers the context customizers * @param contextLoader the resolved {@code ContextLoader} * @param cacheAwareContextLoaderDelegate a cache-aware context loader - * delegate with which to retrieve the parent context + * delegate with which to retrieve the parent {@code ApplicationContext} * @param parent the parent configuration or {@code null} if there is no parent * @since 4.3 + * @deprecated since 6.1 in favor of + * {@link #MergedContextConfiguration(Class, String[], Class[], Set, String[], List, String[], Set, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} */ + @Deprecated(since = "6.1") public MergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, @Nullable Set>> contextInitializerClasses, @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, @@ -244,12 +255,52 @@ public class MergedContextConfiguration implements Serializable { ContextLoader contextLoader, @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { + this(testClass, locations, classes, contextInitializerClasses, activeProfiles, + List.of(new PropertySourceDescriptor(processStrings(propertySourceLocations))), + propertySourceProperties, contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, + parent); + } + + /** + * Create a new {@code MergedContextConfiguration} instance for the supplied + * parameters. + *

If a {@code null} value is supplied for {@code locations}, {@code classes}, + * {@code activeProfiles}, or {@code propertySourceProperties} an empty array + * will be stored instead. If a {@code null} value is supplied for + * {@code contextInitializerClasses} or {@code contextCustomizers}, an empty + * set will be stored instead. Furthermore, active profiles will be sorted, + * and duplicate profiles will be removed. + * @param testClass the test class for which the configuration was merged + * @param locations the merged context resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param propertySourceDescriptors the merged property source descriptors + * @param propertySourceProperties the merged inlined properties + * @param contextCustomizers the context customizers + * @param contextLoader the resolved {@code ContextLoader} + * @param cacheAwareContextLoaderDelegate a cache-aware context loader + * delegate with which to retrieve the parent {@code ApplicationContext} + * @param parent the parent configuration or {@code null} if there is no parent + * @since 6.1 + */ + public MergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + @Nullable Set>> contextInitializerClasses, + @Nullable String[] activeProfiles, List propertySourceDescriptors, + @Nullable String[] propertySourceProperties, @Nullable Set contextCustomizers, + ContextLoader contextLoader, @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, + @Nullable MergedContextConfiguration parent) { + this.testClass = testClass; this.locations = processStrings(locations); this.classes = processClasses(classes); this.contextInitializerClasses = processContextInitializerClasses(contextInitializerClasses); this.activeProfiles = processActiveProfiles(activeProfiles); - this.propertySourceLocations = processStrings(propertySourceLocations); + this.propertySourceDescriptors = Collections.unmodifiableList(propertySourceDescriptors); + this.propertySourceLocations = this.propertySourceDescriptors.stream() + .map(PropertySourceDescriptor::locations) + .flatMap(List::stream) + .toArray(String[]::new); this.propertySourceProperties = processStrings(propertySourceProperties); this.contextCustomizers = processContextCustomizers(contextCustomizers); this.contextLoader = contextLoader; @@ -338,18 +389,32 @@ public class MergedContextConfiguration implements Serializable { } /** - * Get the merged resource locations for test {@code PropertySources} + * Get the merged descriptors for resource locations for test {@code PropertySources} * for the {@linkplain #getTestClass() test class}. + *

Properties will be loaded into the {@code Environment}'s set of + * {@code PropertySources}. + * @since 6.1 + * @see TestPropertySource#locations + * @see TestPropertySource#factory + */ + public List getPropertySourceDescriptors() { + return this.propertySourceDescriptors; + } + + /** + * Get the merged resource locations of properties files for the + * {@linkplain #getTestClass() test class}. * @see TestPropertySource#locations * @see java.util.Properties + * @deprecated since 6.1 in favor of {@link #getPropertySourceDescriptors()} */ + @Deprecated(since = "6.1") public String[] getPropertySourceLocations() { return this.propertySourceLocations; } /** - * Get the merged test {@code PropertySource} properties for the - * {@linkplain #getTestClass() test class}. + * Get the merged inlined properties for the {@linkplain #getTestClass() test class}. *

Properties will be loaded into the {@code Environment}'s set of * {@code PropertySources}. * @see TestPropertySource#properties @@ -408,12 +473,13 @@ public class MergedContextConfiguration implements Serializable { /** * Determine if the supplied object is equal to this {@code MergedContextConfiguration} - * instance by comparing both object's {@linkplain #getLocations() locations}, + * instance by comparing both objects' {@linkplain #getLocations() locations}, * {@linkplain #getClasses() annotated classes}, * {@linkplain #getContextInitializerClasses() context initializer classes}, * {@linkplain #getActiveProfiles() active profiles}, - * {@linkplain #getPropertySourceLocations() property source locations}, + * {@linkplain #getPropertySourceDescriptors() property source descriptors}, * {@linkplain #getPropertySourceProperties() property source properties}, + * {@linkplain #getContextCustomizers() context customizers}, * {@linkplain #getParent() parents}, and the fully qualified names of their * {@link #getContextLoader() ContextLoaders}. */ @@ -439,7 +505,7 @@ public class MergedContextConfiguration implements Serializable { if (!Arrays.equals(this.activeProfiles, otherConfig.activeProfiles)) { return false; } - if (!Arrays.equals(this.propertySourceLocations, otherConfig.propertySourceLocations)) { + if (!this.propertySourceDescriptors.equals(otherConfig.propertySourceDescriptors)) { return false; } if (!Arrays.equals(this.propertySourceProperties, otherConfig.propertySourceProperties)) { @@ -476,7 +542,7 @@ public class MergedContextConfiguration implements Serializable { result = 31 * result + Arrays.hashCode(this.classes); result = 31 * result + this.contextInitializerClasses.hashCode(); result = 31 * result + Arrays.hashCode(this.activeProfiles); - result = 31 * result + Arrays.hashCode(this.propertySourceLocations); + result = 31 * result + this.propertySourceDescriptors.hashCode(); result = 31 * result + Arrays.hashCode(this.propertySourceProperties); result = 31 * result + this.contextCustomizers.hashCode(); result = 31 * result + (this.parent != null ? this.parent.hashCode() : 0); @@ -489,7 +555,7 @@ public class MergedContextConfiguration implements Serializable { * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes}, * {@linkplain #getContextInitializerClasses() context initializer classes}, * {@linkplain #getActiveProfiles() active profiles}, - * {@linkplain #getPropertySourceLocations() property source locations}, + * {@linkplain #getPropertySourceDescriptors() property source descriptors}, * {@linkplain #getPropertySourceProperties() property source properties}, * {@linkplain #getContextCustomizers() context customizers}, * the name of the {@link #getContextLoader() ContextLoader}, and the @@ -503,7 +569,7 @@ public class MergedContextConfiguration implements Serializable { .append("classes", this.classes) .append("contextInitializerClasses", this.contextInitializerClasses) .append("activeProfiles", this.activeProfiles) - .append("propertySourceLocations", this.propertySourceLocations) + .append("propertySourceDescriptors", this.propertySourceDescriptors) .append("propertySourceProperties", this.propertySourceProperties) .append("contextCustomizers", this.contextCustomizers) .append("contextLoader", (this.contextLoader != null ? this.contextLoader.getClass() : null)) @@ -512,7 +578,7 @@ public class MergedContextConfiguration implements Serializable { } - private static String[] processStrings(@Nullable String[] array) { + protected static String[] processStrings(@Nullable String[] array) { return (array != null ? array : EMPTY_STRING_ARRAY); } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java index 337df27254..a2ffa04723 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -25,6 +25,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.io.support.PropertySourceFactory; /** * {@code @TestPropertySource} is a class-level annotation that is used to @@ -113,9 +114,10 @@ public @interface TestPropertySource { * will be added to the enclosing {@code Environment} as its own property * source, in the order declared. *

Supported File Formats

- *

Both traditional and XML-based properties file formats are supported - * — for example, {@code "classpath:/com/example/test.properties"} - * or {@code "file:/path/to/file.xml"}. + *

By default, both traditional and XML-based properties file formats are + * supported — for example, {@code "classpath:/com/example/test.properties"} + * or {@code "file:/path/to/file.xml"}. To support a different file format, + * configure an appropriate {@link #factory() PropertySourceFactory}. *

Path Resource Semantics

*

Each path will be interpreted as a Spring * {@link org.springframework.core.io.Resource Resource}. A plain path @@ -129,9 +131,8 @@ public @interface TestPropertySource { * {@link org.springframework.util.ResourceUtils#FILE_URL_PREFIX file:}, * {@code http:}, etc.) will be loaded using the specified resource protocol. * Resource location wildcards (e.g. **/*.properties) - * are not permitted: each location must evaluate to exactly one - * {@code .properties} or {@code .xml} resource. Property placeholders - * in paths (i.e., ${...}) will be + * are not permitted: each location must evaluate to exactly one properties + * resource. Property placeholders in paths (i.e., ${...}) will be * {@linkplain org.springframework.core.env.Environment#resolveRequiredPlaceholders(String) resolved} * against the {@code Environment}. *

Default Properties File Detection

@@ -144,6 +145,7 @@ public @interface TestPropertySource { * @see #inheritLocations * @see #value * @see #properties + * @see #factory * @see org.springframework.core.env.PropertySource */ @AliasFor("value") @@ -278,4 +280,15 @@ public @interface TestPropertySource { */ boolean inheritProperties() default true; + /** + * Specify a custom {@link PropertySourceFactory}, if any. + *

By default, a factory for standard resource files will be used which + * supports {@code *.properties} and {@code *.xml} file formats for + * {@link java.util.Properties}. + * @since 6.1 + * @see org.springframework.core.io.support.DefaultPropertySourceFactory + * @see org.springframework.core.io.support.ResourcePropertySource + */ + Class factory() default PropertySourceFactory.class; + } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java b/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java index c68e7254ac..715f09ec37 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -53,6 +53,7 @@ class MergedContextConfigurationRuntimeHints { private static final Method getResourceBasePathMethod = loadGetResourceBasePathMethod(); + @SuppressWarnings("deprecation") public void registerHints(RuntimeHints runtimeHints, MergedContextConfiguration mergedConfig, ClassLoader classLoader) { // @ContextConfiguration(loader = ...) ContextLoader contextLoader = mergedConfig.getContextLoader(); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java index 35759fbe51..222f25ec45 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java @@ -127,15 +127,15 @@ public abstract class AbstractContextLoader implements SmartContextLoader { * @param context the newly created application context * @param mergedConfig the merged context configuration * @since 3.2 - * @see TestPropertySourceUtils#addPropertiesFilesToEnvironment - * @see TestPropertySourceUtils#addInlinedPropertiesToEnvironment + * @see TestPropertySourceUtils#addPropertySourcesToEnvironment(ConfigurableApplicationContext, List) + * @see TestPropertySourceUtils#addInlinedPropertiesToEnvironment(ConfigurableApplicationContext, String...) * @see ApplicationContextInitializer#initialize(ConfigurableApplicationContext) * @see #loadContext(MergedContextConfiguration) * @see ConfigurableApplicationContext#setId */ protected void prepareContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles()); - TestPropertySourceUtils.addPropertiesFilesToEnvironment(context, mergedConfig.getPropertySourceLocations()); + TestPropertySourceUtils.addPropertySourcesToEnvironment(context, mergedConfig.getPropertySourceDescriptors()); TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, mergedConfig.getPropertySourceProperties()); invokeApplicationContextInitializers(context, mergedConfig); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index 8fa81c4f62..2343bb5d86 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -368,7 +368,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot StringUtils.toStringArray(locations), ClassUtils.toClassArray(classes), ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList), ActiveProfilesUtils.resolveActiveProfiles(testClass), - mergedTestPropertySources.getLocations(), + mergedTestPropertySources.getPropertySourceDescriptors(), mergedTestPropertySources.getProperties(), contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java b/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java index 89e985808f..6c89dad7a4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -17,7 +17,9 @@ package org.springframework.test.context.support; import java.util.Arrays; +import java.util.List; +import org.springframework.core.io.support.PropertySourceDescriptor; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; @@ -36,9 +38,9 @@ import org.springframework.util.Assert; */ class MergedTestPropertySources { - private static final MergedTestPropertySources empty = new MergedTestPropertySources(new String[0], new String[0]); + private static final MergedTestPropertySources empty = new MergedTestPropertySources(List.of(), new String[0]); - private final String[] locations; + private final List descriptors; private final String[] properties; @@ -53,25 +55,25 @@ class MergedTestPropertySources { /** * Create a {@code MergedTestPropertySources} instance with the supplied - * {@code locations} and {@code properties}. - * @param locations the resource locations of properties files; may be - * empty but never {@code null} + * {@code descriptors} and {@code properties}. + * @param descriptors the descriptors for resource locations + * of properties files; may be empty but never {@code null} * @param properties the properties in the form of {@code key=value} pairs; * may be empty but never {@code null} */ - MergedTestPropertySources(String[] locations, String[] properties) { - Assert.notNull(locations, "The locations array must not be null"); + MergedTestPropertySources(List descriptors, String[] properties) { + Assert.notNull(descriptors, "The descriptors list must not be null"); Assert.notNull(properties, "The properties array must not be null"); - this.locations = locations; + this.descriptors = descriptors; this.properties = properties; } /** - * Get the resource locations of properties files. + * Get the descriptors for resource locations of properties files. * @see TestPropertySource#locations() */ - String[] getLocations() { - return this.locations; + List getPropertySourceDescriptors() { + return this.descriptors; } /** @@ -84,8 +86,8 @@ class MergedTestPropertySources { /** * Determine if the supplied object is equal to this {@code MergedTestPropertySources} - * instance by comparing both object's {@linkplain #getLocations() locations} - * and {@linkplain #getProperties() properties}. + * instance by comparing both objects' {@linkplain #getPropertySourceDescriptors() + * descriptors} and {@linkplain #getProperties() properties}. * @since 5.3 */ @Override @@ -98,7 +100,7 @@ class MergedTestPropertySources { } MergedTestPropertySources that = (MergedTestPropertySources) other; - if (!Arrays.equals(this.locations, that.locations)) { + if (!this.descriptors.equals(that.descriptors)) { return false; } if (!Arrays.equals(this.properties, that.properties)) { @@ -115,7 +117,7 @@ class MergedTestPropertySources { */ @Override public int hashCode() { - int result = Arrays.hashCode(this.locations); + int result = this.descriptors.hashCode(); result = 31 * result + Arrays.hashCode(this.properties); return result; } @@ -128,7 +130,7 @@ class MergedTestPropertySources { @Override public String toString() { return new ToStringCreator(this, new DefaultToStringStyler(new SimpleValueStyler())) - .append("locations", this.locations) + .append("descriptors", this.descriptors) .append("properties", this.properties) .toString(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java index 25f5ad158d..3d08ad0e73 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -25,12 +25,15 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.PropertySourceDescriptor; +import org.springframework.core.io.support.PropertySourceFactory; import org.springframework.core.log.LogMessage; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.util.TestContextResourceUtils; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -61,7 +64,7 @@ class TestPropertySourceAttributes { private final MergedAnnotation rootAnnotation; - private final List locations = new ArrayList<>(); + private final List descriptors = new ArrayList<>(); private final boolean inheritLocations; @@ -70,12 +73,13 @@ class TestPropertySourceAttributes { private final boolean inheritProperties; - TestPropertySourceAttributes(MergedAnnotation annotation) { - this.declaringClass = declaringClass(annotation); - this.rootAnnotation = annotation.getRoot(); - this.inheritLocations = annotation.getBoolean("inheritLocations"); - this.inheritProperties = annotation.getBoolean("inheritProperties"); - addPropertiesAndLocationsFrom(annotation); + @SuppressWarnings("unchecked") + TestPropertySourceAttributes(MergedAnnotation mergedAnnotation) { + this.declaringClass = declaringClass(mergedAnnotation); + this.rootAnnotation = mergedAnnotation.getRoot(); + this.inheritLocations = mergedAnnotation.getBoolean("inheritLocations"); + this.inheritProperties = mergedAnnotation.getBoolean("inheritProperties"); + addPropertiesAndLocationsFrom(mergedAnnotation, this.declaringClass); } /** @@ -112,29 +116,52 @@ class TestPropertySourceAttributes { attributeName)); } - private void addPropertiesAndLocationsFrom(MergedAnnotation mergedAnnotation) { + @SuppressWarnings("unchecked") + private void addPropertiesAndLocationsFrom(MergedAnnotation mergedAnnotation, + Class declaringClass) { + String[] locations = mergedAnnotation.getStringArray("locations"); String[] properties = mergedAnnotation.getStringArray("properties"); - addPropertiesAndLocations(locations, properties, declaringClass(mergedAnnotation), false); + String[] convertedLocations = + TestContextResourceUtils.convertToClasspathResourcePaths(declaringClass, true, locations); + Class factoryClass = + (Class) mergedAnnotation.getClass("factory"); + PropertySourceDescriptor descriptor = new PropertySourceDescriptor( + Arrays.asList(convertedLocations), false, null, factoryClass, null); + addPropertiesAndLocations(List.of(descriptor), properties, declaringClass, false); } private void mergePropertiesAndLocationsFrom(TestPropertySourceAttributes attributes) { - addPropertiesAndLocations(attributes.getLocations(), attributes.getProperties(), + addPropertiesAndLocations(attributes.getPropertySourceDescriptors(), attributes.getProperties(), attributes.getDeclaringClass(), true); } - private void addPropertiesAndLocations(String[] locations, String[] properties, + private void addPropertiesAndLocations(List descriptors, String[] properties, Class declaringClass, boolean prepend) { - if (ObjectUtils.isEmpty(locations) && ObjectUtils.isEmpty(properties)) { - addAll(prepend, this.locations, detectDefaultPropertiesFile(declaringClass)); + if (hasNoLocations(descriptors) && ObjectUtils.isEmpty(properties)) { + String defaultPropertiesFile = detectDefaultPropertiesFile(declaringClass); + addAll(prepend, this.descriptors, List.of(new PropertySourceDescriptor(defaultPropertiesFile))); } else { - addAll(prepend, this.locations, locations); + addAll(prepend, this.descriptors, descriptors); addAll(prepend, this.properties, properties); } } + /** + * Add all the supplied elements to the provided list, honoring the + * {@code prepend} flag. + *

If the {@code prepend} flag is {@code false}, the elements will be appended + * to the list. + * @param prepend whether the elements should be prepended to the list + * @param list the list to which to add the elements + * @param elements the elements to add to the list + */ + private void addAll(boolean prepend, List list, List elements) { + list.addAll((prepend ? 0 : list.size()), elements); + } + /** * Add all the supplied elements to the provided list, honoring the * {@code prepend} flag. @@ -177,16 +204,18 @@ class TestPropertySourceAttributes { } /** - * Get the resource locations that were declared via {@code @TestPropertySource}. + * Get the descriptors for resource locations that were declared via + * {@code @TestPropertySource}. *

Note: The returned value may represent a detected default * or merged locations that do not match the original value declared via a * single {@code @TestPropertySource} annotation. - * @return the resource locations; potentially empty + * @return the resource location descriptors; potentially empty * @see TestPropertySource#value * @see TestPropertySource#locations + * @see TestPropertySource#factory */ - String[] getLocations() { - return StringUtils.toStringArray(this.locations); + List getPropertySourceDescriptors() { + return this.descriptors; } /** @@ -220,7 +249,7 @@ class TestPropertySourceAttributes { } boolean isEmpty() { - return (this.locations.isEmpty() && this.properties.isEmpty()); + return (hasNoLocations(this.descriptors) && this.properties.isEmpty()); } @Override @@ -233,7 +262,7 @@ class TestPropertySourceAttributes { } TestPropertySourceAttributes that = (TestPropertySourceAttributes) other; - if (!this.locations.equals(that.locations)) { + if (!this.descriptors.equals(that.descriptors)) { return false; } if (!this.properties.equals(that.properties)) { @@ -251,7 +280,7 @@ class TestPropertySourceAttributes { @Override public int hashCode() { - int result = this.locations.hashCode(); + int result = this.descriptors.hashCode(); result = 31 * result + this.properties.hashCode(); result = 31 * result + (this.inheritLocations ? 1231 : 1237); result = 31 * result + (this.inheritProperties ? 1231 : 1237); @@ -265,8 +294,8 @@ class TestPropertySourceAttributes { @Override public String toString() { return new ToStringCreator(this, new DefaultToStringStyler(new SimpleValueStyler())) - .append("declaringClass", this.declaringClass) - .append("locations", this.locations) + .append("declaringClass", this.declaringClass.getName()) + .append("descriptors", this.descriptors) .append("inheritLocations", this.inheritLocations) .append("properties", this.properties) .append("inheritProperties", this.inheritProperties) @@ -279,4 +308,12 @@ class TestPropertySourceAttributes { return (Class) source; } + /** + * Determine if the supplied list contains no descriptor with locations. + */ + private static boolean hasNoLocations(List descriptors) { + return descriptors.stream().map(PropertySourceDescriptor::locations) + .flatMap(List::stream).findAny().isEmpty(); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java index 64bfb11589..66bd06f529 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -30,6 +30,7 @@ import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanUtils; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -37,15 +38,18 @@ import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.core.io.support.ResourcePropertySource; +import org.springframework.core.io.support.DefaultPropertySourceFactory; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertySourceDescriptor; +import org.springframework.core.io.support.PropertySourceFactory; import org.springframework.lang.Nullable; import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.util.TestContextResourceUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -71,6 +75,8 @@ public abstract class TestPropertySourceUtils { */ public static final String INLINED_PROPERTIES_PROPERTY_SOURCE_NAME = "Inlined Test Properties"; + private static final PropertySourceFactory defaultPropertySourceFactory = new DefaultPropertySourceFactory(); + private static final Log logger = LogFactory.getLog(TestPropertySourceUtils.class); @@ -139,20 +145,18 @@ public abstract class TestPropertySourceUtils { return duplicationDetected; } - private static String[] mergeLocations(List attributesList) { - List locations = new ArrayList<>(); + private static List mergeLocations(List attributesList) { + List descriptors = new ArrayList<>(); for (TestPropertySourceAttributes attrs : attributesList) { if (logger.isTraceEnabled()) { logger.trace("Processing locations for " + attrs); } - String[] locationsArray = TestContextResourceUtils.convertToClasspathResourcePaths( - attrs.getDeclaringClass(), true, attrs.getLocations()); - locations.addAll(0, Arrays.asList(locationsArray)); + descriptors.addAll(0, attrs.getPropertySourceDescriptors()); if (!attrs.isInheritLocations()) { break; } } - return StringUtils.toStringArray(locations); + return descriptors; } private static String[] mergeProperties(List attributesList) { @@ -181,9 +185,10 @@ public abstract class TestPropertySourceUtils { * to the environment; potentially empty but never {@code null} * @throws IllegalStateException if an error occurs while processing a properties file * @since 4.1.5 - * @see ResourcePropertySource + * @see org.springframework.core.io.support.ResourcePropertySource * @see TestPropertySource#locations * @see #addPropertiesFilesToEnvironment(ConfigurableEnvironment, ResourceLoader, String...) + * @see #addPropertySourcesToEnvironment(ConfigurableApplicationContext, List) */ public static void addPropertiesFilesToEnvironment(ConfigurableApplicationContext context, String... locations) { Assert.notNull(context, "'context' must not be null"); @@ -197,7 +202,8 @@ public abstract class TestPropertySourceUtils { *

Property placeholders in resource locations (i.e., ${...}) * will be {@linkplain Environment#resolveRequiredPlaceholders(String) resolved} * against the {@code Environment}. - *

Each properties file will be converted to a {@link ResourcePropertySource} + *

Each properties file will be converted to a + * {@link org.springframework.core.io.support.ResourcePropertySource ResourcePropertySource} * that will be added to the {@link PropertySources} of the environment with * the highest precedence. * @param environment the environment to update; never {@code null} @@ -207,21 +213,95 @@ public abstract class TestPropertySourceUtils { * to the environment; potentially empty but never {@code null} * @throws IllegalStateException if an error occurs while processing a properties file * @since 4.3 - * @see ResourcePropertySource + * @see org.springframework.core.io.support.ResourcePropertySource * @see TestPropertySource#locations * @see #addPropertiesFilesToEnvironment(ConfigurableApplicationContext, String...) + * @see #addPropertySourcesToEnvironment(ConfigurableApplicationContext, List) */ public static void addPropertiesFilesToEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader, String... locations) { + Assert.notNull(locations, "'locations' must not be null"); + addPropertySourcesToEnvironment(environment, resourceLoader, + List.of(new PropertySourceDescriptor(locations))); + } + + /** + * Add property sources for the given {@code descriptors} to the + * {@link Environment} of the supplied {@code context}. + *

Property placeholders in resource locations (i.e., ${...}) + * will be {@linkplain Environment#resolveRequiredPlaceholders(String) resolved} + * against the {@code Environment}. + *

Each {@link PropertySource} will be created via the configured + * {@link PropertySourceDescriptor#propertySourceFactory() PropertySourceFactory} + * (or the {@link DefaultPropertySourceFactory} if no factory is configured) + * and added to the {@link PropertySources} of the environment with the highest + * precedence. + * @param context the application context whose environment should be updated; + * never {@code null} + * @param descriptors the property source descriptors to process; potentially + * empty but never {@code null} + * @throws IllegalStateException if an error occurs while processing the + * descriptors and registering property sources + * @since 6.1 + * @see TestPropertySource#locations + * @see TestPropertySource#factory + * @see PropertySourceFactory + */ + public static void addPropertySourcesToEnvironment(ConfigurableApplicationContext context, + List descriptors) { + + Assert.notNull(context, "'context' must not be null"); + Assert.notNull(descriptors, "'descriptors' must not be null"); + addPropertySourcesToEnvironment(context.getEnvironment(), context, descriptors); + } + + /** + * Add property sources for the given {@code descriptors} to the supplied + * {@link ConfigurableEnvironment environment}. + *

Property placeholders in resource locations (i.e., ${...}) + * will be {@linkplain Environment#resolveRequiredPlaceholders(String) resolved} + * against the {@code Environment}. + *

Each {@link PropertySource} will be created via the configured + * {@link PropertySourceDescriptor#propertySourceFactory() PropertySourceFactory} + * (or the {@link DefaultPropertySourceFactory} if no factory is configured) + * and added to the {@link PropertySources} of the environment with the highest + * precedence. + * @param environment the environment to update; never {@code null} + * @param resourceLoader the {@code ResourceLoader} to use to load each resource; + * never {@code null} + * @param descriptors the property source descriptors to process; potentially + * empty but never {@code null} + * @throws IllegalStateException if an error occurs while processing the + * descriptors and registering property sources + * @since 6.1 + * @see TestPropertySource#locations + * @see TestPropertySource#factory + * @see PropertySourceFactory + */ + private static void addPropertySourcesToEnvironment(ConfigurableEnvironment environment, + ResourceLoader resourceLoader, List descriptors) { + Assert.notNull(environment, "'environment' must not be null"); Assert.notNull(resourceLoader, "'resourceLoader' must not be null"); - Assert.notNull(locations, "'locations' must not be null"); + Assert.notNull(descriptors, "'descriptors' must not be null"); + MutablePropertySources propertySources = environment.getPropertySources(); try { - for (String location : locations) { - String resolvedLocation = environment.resolveRequiredPlaceholders(location); - Resource resource = resourceLoader.getResource(resolvedLocation); - environment.getPropertySources().addFirst(new ResourcePropertySource(resource)); + for (PropertySourceDescriptor descriptor : descriptors) { + if (!descriptor.locations().isEmpty()) { + Class factoryClass = descriptor.propertySourceFactory(); + PropertySourceFactory factory = + (factoryClass != null && factoryClass != PropertySourceFactory.class ? + BeanUtils.instantiateClass(factoryClass) : defaultPropertySourceFactory); + + for (String location : descriptor.locations()) { + String resolvedLocation = environment.resolveRequiredPlaceholders(location); + Resource resource = resourceLoader.getResource(resolvedLocation); + PropertySource propertySource = factory.createPropertySource(descriptor.name(), + new EncodedResource(resource, descriptor.encoding())); + propertySources.addFirst(propertySource); + } + } } } catch (IOException ex) { diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java index 556779d0a7..274af31041 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -16,9 +16,11 @@ package org.springframework.test.context.web; +import java.util.List; import java.util.Set; import org.springframework.context.ApplicationContextInitializer; +import org.springframework.core.io.support.PropertySourceDescriptor; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; @@ -99,7 +101,10 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { * delegate with which to retrieve the parent context * @param parent the parent configuration or {@code null} if there is no parent * @since 4.1 + * @deprecated since 6.1 in favor of + * {@link #WebMergedContextConfiguration(Class, String[], Class[], Set, String[], List, String[], Set, String, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} */ + @Deprecated(since = "6.1") public WebMergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, @Nullable Set>> contextInitializerClasses, @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, @Nullable String[] propertySourceProperties, @@ -135,14 +140,54 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { * delegate with which to retrieve the parent context * @param parent the parent configuration or {@code null} if there is no parent * @since 4.3 + * @deprecated since 6.1 in favor of + * {@link #WebMergedContextConfiguration(Class, String[], Class[], Set, String[], List, String[], Set, String, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} */ + @Deprecated(since = "6.1") public WebMergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, @Nullable Set>> contextInitializerClasses, @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, @Nullable String[] propertySourceProperties, @Nullable Set contextCustomizers, String resourceBasePath, ContextLoader contextLoader, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { - super(testClass, locations, classes, contextInitializerClasses, activeProfiles, propertySourceLocations, + this(testClass, locations, classes, contextInitializerClasses, activeProfiles, + List.of(new PropertySourceDescriptor(processStrings(propertySourceLocations))), + propertySourceProperties, contextCustomizers, resourceBasePath, contextLoader, + cacheAwareContextLoaderDelegate, parent); + } + + /** + * Create a new {@code WebMergedContextConfiguration} instance for the supplied + * parameters. + *

If a {@code null} value is supplied for {@code locations}, {@code classes}, + * {@code activeProfiles}, or {@code propertySourceProperties} an empty array + * will be stored instead. If a {@code null} value is supplied for + * {@code contextInitializerClasses} or {@code contextCustomizers}, an empty + * set will be stored instead. Furthermore, active profiles will be sorted, + * and duplicate profiles will be removed. + * @param testClass the test class for which the configuration was merged + * @param locations the merged context resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param propertySourceDescriptors the merged property source descriptors + * @param propertySourceProperties the merged inlined properties + * @param contextCustomizers the context customizers + * @param resourceBasePath the resource path to the root directory of the web application + * @param contextLoader the resolved {@code ContextLoader} + * @param cacheAwareContextLoaderDelegate a cache-aware context loader + * delegate with which to retrieve the parent {@code ApplicationContext} + * @param parent the parent configuration or {@code null} if there is no parent + * @since 6.1 + */ + public WebMergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + @Nullable Set>> contextInitializerClasses, + @Nullable String[] activeProfiles, + List propertySourceDescriptors, @Nullable String[] propertySourceProperties, + @Nullable Set contextCustomizers, String resourceBasePath, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { + + super(testClass, locations, classes, contextInitializerClasses, activeProfiles, propertySourceDescriptors, propertySourceProperties, contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, parent); this.resourceBasePath = (StringUtils.hasText(resourceBasePath) ? resourceBasePath : ""); @@ -160,11 +205,14 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { /** * Determine if the supplied object is equal to this {@code WebMergedContextConfiguration} - * instance by comparing both object's {@linkplain #getLocations() locations}, + * instance by comparing both objects' {@linkplain #getLocations() locations}, * {@linkplain #getClasses() annotated classes}, * {@linkplain #getContextInitializerClasses() context initializer classes}, * {@linkplain #getActiveProfiles() active profiles}, - * {@linkplain #getResourceBasePath() resource base path}, + * {@linkplain #getResourceBasePath() resource base paths}, + * {@linkplain #getPropertySourceDescriptors() property source descriptors}, + * {@linkplain #getPropertySourceProperties() property source properties}, + * {@linkplain #getContextCustomizers() context customizers}, * {@linkplain #getParent() parents}, and the fully qualified names of their * {@link #getContextLoader() ContextLoaders}. */ @@ -189,7 +237,7 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes}, * {@linkplain #getContextInitializerClasses() context initializer classes}, * {@linkplain #getActiveProfiles() active profiles}, - * {@linkplain #getPropertySourceLocations() property source locations}, + * {@linkplain #getPropertySourceDescriptors() property source descriptors}, * {@linkplain #getPropertySourceProperties() property source properties}, * {@linkplain #getContextCustomizers() context customizers}, * {@linkplain #getResourceBasePath() resource base path}, the name of the @@ -204,7 +252,7 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { .append("classes", getClasses()) .append("contextInitializerClasses", getContextInitializerClasses()) .append("activeProfiles", getActiveProfiles()) - .append("propertySourceLocations", getPropertySourceLocations()) + .append("propertySourceDescriptors", getPropertySourceDescriptors()) .append("propertySourceProperties", getPropertySourceProperties()) .append("contextCustomizers", getContextCustomizers()) .append("resourceBasePath", getResourceBasePath()) diff --git a/spring-test/src/test/java/org/springframework/test/context/MergedContextConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/MergedContextConfigurationTests.java index 85cb818618..f948e2ed6a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/MergedContextConfigurationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/MergedContextConfigurationTests.java @@ -18,6 +18,7 @@ package org.springframework.test.context; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; @@ -409,9 +410,9 @@ class MergedContextConfigurationTests { void equalsWithSameContextCustomizers() { Set customizers = Collections.singleton(mock()); MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, - EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, null, null, customizers, loader, null, null); + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, List.of(), null, customizers, loader, null, null); MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, - EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, null, null, customizers, loader, null, null); + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, List.of(), null, customizers, loader, null, null); assertThat(mergedConfig2).isEqualTo(mergedConfig1); } @@ -424,9 +425,9 @@ class MergedContextConfigurationTests { Set customizers2 = Collections.singleton(mock()); MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, - EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, null, null, customizers1, loader, null, null); + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, List.of(), null, customizers1, loader, null, null); MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, - EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, null, null, customizers2, loader, null, null); + EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, List.of(), null, customizers2, loader, null, null); assertThat(mergedConfig2).isNotEqualTo(mergedConfig1); assertThat(mergedConfig1).isNotEqualTo(mergedConfig2); } diff --git a/spring-test/src/test/java/org/springframework/test/context/env/YamlPropertySourceFactory.java b/spring-test/src/test/java/org/springframework/test/context/env/YamlPropertySourceFactory.java new file mode 100644 index 0000000000..d499ac78b6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/env/YamlPropertySourceFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2023 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.test.context.env; + +import java.util.Properties; + +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertySourceFactory; +import org.springframework.util.StringUtils; + +/** + * Demo {@link PropertySourceFactory} that provides YAML support. + * + * @author Sam Brannen + * @since 6.1 + */ +class YamlPropertySourceFactory implements PropertySourceFactory { + + @Override + public PropertySource createPropertySource(String name, EncodedResource encodedResource) { + Resource resource = encodedResource.getResource(); + if (!StringUtils.hasText(name)) { + name = getNameForResource(resource); + } + YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean(); + factoryBean.setResources(resource); + factoryBean.afterPropertiesSet(); + Properties properties = factoryBean.getObject(); + return new PropertiesPropertySource(name, properties); + } + + /** + * Return the description for the given Resource; if the description is + * empty, return the class name of the resource plus its identity hash code. + */ + private static String getNameForResource(Resource resource) { + String name = resource.getDescription(); + if (!StringUtils.hasText(name)) { + name = resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource); + } + return name; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/env/YamlTestProperties.java b/spring-test/src/test/java/org/springframework/test/context/env/YamlTestProperties.java new file mode 100644 index 0000000000..e796874bcc --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/env/YamlTestProperties.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2023 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.test.context.env; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Sam Brannen + * @since 6.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@TestPropertySource(factory = YamlPropertySourceFactory.class) +public @interface YamlTestProperties { + + @AliasFor(annotation = TestPropertySource.class) + String[] value(); + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/env/YamlTestPropertySourceTests.java b/spring-test/src/test/java/org/springframework/test/context/env/YamlTestPropertySourceTests.java new file mode 100644 index 0000000000..046e6b428c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/env/YamlTestPropertySourceTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2023 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.test.context.env; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.io.support.PropertySourceFactory; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link TestPropertySource @TestPropertySource} support + * with a custom YAML {@link PropertySourceFactory}. + * + * @author Sam Brannen + * @since 6.1 + */ +@SpringJUnitConfig +@YamlTestProperties("test-properties.yaml") +class YamlTestPropertySourceTests { + + @ParameterizedTest + @CsvSource(delimiterString = "->", textBlock = """ + environments.dev.url -> https://dev.example.com + environments.dev.name -> 'Developer Setup' + environments.prod.url -> https://prod.example.com + environments.prod.name -> 'My Cool App' + """) + void propertyIsAvailableInEnvironment(String property, String value, @Autowired ConfigurableEnvironment env) { + assertThat(env.getProperty(property)).isEqualTo(value); + } + + + @Configuration + static class Config { + /* no user beans required for these tests */ + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalPropertiesFileAndMetaPropertiesFileTests.java b/spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalPropertiesFileAndMetaPropertiesFileTests.java index 405f92eb78..dd5e19e0f5 100644 --- a/spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalPropertiesFileAndMetaPropertiesFileTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalPropertiesFileAndMetaPropertiesFileTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -16,15 +16,9 @@ package org.springframework.test.context.env.repeatable; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - import org.junit.jupiter.api.Test; import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.env.repeatable.LocalPropertiesFileAndMetaPropertiesFileTests.MetaFileTestProperty; /** * Integration tests for {@link TestPropertySource @TestPropertySource} as a @@ -48,15 +42,4 @@ class LocalPropertiesFileAndMetaPropertiesFileTests extends AbstractRepeatableTe assertEnvironmentValue("key2", "meta file"); } - - /** - * Composed annotation that declares a properties file via - * {@link TestPropertySource @TestPropertySource}. - */ - @Target(ElementType.TYPE) - @Retention(RetentionPolicy.RUNTIME) - @TestPropertySource("meta.properties") - @interface MetaFileTestProperty { - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalYamlFileAndMetaPropertiesFileTests.java b/spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalYamlFileAndMetaPropertiesFileTests.java new file mode 100644 index 0000000000..a78f4f9f1c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalYamlFileAndMetaPropertiesFileTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2023 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.test.context.env.repeatable; + +import org.junit.jupiter.api.Test; + +import org.springframework.test.context.env.YamlTestProperties; + +/** + * Analogous to {@link LocalPropertiesFileAndMetaPropertiesFileTests} except + * that the local file is YAML. + * + * @author Sam Brannen + * @since 6.1 + */ +@YamlTestProperties("local.yaml") +@MetaFileTestProperty +class LocalYamlFileAndMetaPropertiesFileTests extends AbstractRepeatableTestPropertySourceTests { + + @Test + void test() { + assertEnvironmentValue("key1", "local file"); + assertEnvironmentValue("key2", "meta file"); + + assertEnvironmentValue("environments.dev.url", "https://dev.example.com"); + assertEnvironmentValue("environments.dev.name", "Developer Setup"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/env/repeatable/MetaFileTestProperty.java b/spring-test/src/test/java/org/springframework/test/context/env/repeatable/MetaFileTestProperty.java new file mode 100644 index 0000000000..ca6e7b4702 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/env/repeatable/MetaFileTestProperty.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2023 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.test.context.env.repeatable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.test.context.TestPropertySource; + +/** + * Composed annotation that declares a properties file via + * {@link TestPropertySource @TestPropertySource}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@TestPropertySource("meta.properties") +@interface MetaFileTestProperty { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java index a6a8381a4a..8a3e8cd795 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -187,6 +187,7 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt assertMergedConfigForLocationPaths(RelativeFooXmlLocation.class); } + @SuppressWarnings("deprecation") private void assertMergedConfigForLocationPaths(Class testClass) { MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); diff --git a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java index 490d5a3b6e..7eeb91b753 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java @@ -18,7 +18,9 @@ package org.springframework.test.context.support; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; @@ -29,6 +31,7 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PropertySourceDescriptor; import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.env.MockPropertySource; import org.springframework.test.context.TestPropertySource; @@ -295,7 +298,9 @@ class TestPropertySourceUtilsTests { MergedTestPropertySources mergedPropertySources = buildMergedTestPropertySources(testClass); SoftAssertions.assertSoftly(softly -> { softly.assertThat(mergedPropertySources).isNotNull(); - softly.assertThat(mergedPropertySources.getLocations()).isEqualTo(expectedLocations); + Stream locations = mergedPropertySources.getPropertySourceDescriptors().stream() + .map(PropertySourceDescriptor::locations).flatMap(List::stream); + softly.assertThat(locations).containsExactly(expectedLocations); softly.assertThat(mergedPropertySources.getProperties()).isEqualTo(expectedProperties); }); } diff --git a/spring-test/src/test/java/org/springframework/test/context/web/AnnotationConfigWebContextLoaderTests.java b/spring-test/src/test/java/org/springframework/test/context/web/AnnotationConfigWebContextLoaderTests.java index 660f7c18f6..e6a180d938 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/AnnotationConfigWebContextLoaderTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/AnnotationConfigWebContextLoaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -33,14 +33,15 @@ class AnnotationConfigWebContextLoaderTests { @Test - void configMustNotContainLocations() throws Exception { + @SuppressWarnings("deprecation") + void configMustNotContainLocations() { AnnotationConfigWebContextLoader loader = new AnnotationConfigWebContextLoader(); WebMergedContextConfiguration mergedConfig = new WebMergedContextConfiguration(getClass(), new String[] { "config.xml" }, EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY, "resource/path", loader, null, null); assertThatIllegalStateException() - .isThrownBy(() -> loader.loadContext(mergedConfig)) - .withMessageContaining("does not support resource locations"); + .isThrownBy(() -> loader.loadContext(mergedConfig)) + .withMessageContaining("does not support resource locations"); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/web/GenericXmlWebContextLoaderTests.java b/spring-test/src/test/java/org/springframework/test/context/web/GenericXmlWebContextLoaderTests.java index e127088da3..23b10918c3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/GenericXmlWebContextLoaderTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/GenericXmlWebContextLoaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -34,12 +34,13 @@ class GenericXmlWebContextLoaderTests { @Test void configMustNotContainAnnotatedClasses() throws Exception { GenericXmlWebContextLoader loader = new GenericXmlWebContextLoader(); + @SuppressWarnings("deprecation") WebMergedContextConfiguration mergedConfig = new WebMergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY, new Class[] { getClass() }, null, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY, "resource/path", loader, null, null); assertThatIllegalStateException() - .isThrownBy(() -> loader.loadContext(mergedConfig)) - .withMessageContaining("does not support annotated classes"); + .isThrownBy(() -> loader.loadContext(mergedConfig)) + .withMessageContaining("does not support annotated classes"); } } diff --git a/spring-test/src/test/resources/org/springframework/test/context/env/repeatable/local.yaml b/spring-test/src/test/resources/org/springframework/test/context/env/repeatable/local.yaml new file mode 100644 index 0000000000..beacfb5f1f --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/env/repeatable/local.yaml @@ -0,0 +1,5 @@ +key1: local file +environments: + dev: + url: https://dev.example.com + name: Developer Setup diff --git a/spring-test/src/test/resources/org/springframework/test/context/env/test-properties.yaml b/spring-test/src/test/resources/org/springframework/test/context/env/test-properties.yaml new file mode 100644 index 0000000000..8309709490 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/env/test-properties.yaml @@ -0,0 +1,7 @@ +environments: + dev: + url: https://dev.example.com + name: Developer Setup + prod: + url: https://prod.example.com + name: My Cool App