Browse Source

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
pull/31006/head
Sam Brannen 2 years ago
parent
commit
04cce0bafd
  1. 13
      framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc
  2. 1
      spring-test/spring-test.gradle
  3. 102
      spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java
  4. 27
      spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java
  5. 3
      spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java
  6. 6
      spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java
  7. 2
      spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java
  8. 36
      spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java
  9. 85
      spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java
  10. 114
      spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java
  11. 60
      spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java
  12. 9
      spring-test/src/test/java/org/springframework/test/context/MergedContextConfigurationTests.java
  13. 62
      spring-test/src/test/java/org/springframework/test/context/env/YamlPropertySourceFactory.java
  14. 39
      spring-test/src/test/java/org/springframework/test/context/env/YamlTestProperties.java
  15. 59
      spring-test/src/test/java/org/springframework/test/context/env/YamlTestPropertySourceTests.java
  16. 19
      spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalPropertiesFileAndMetaPropertiesFileTests.java
  17. 43
      spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalYamlFileAndMetaPropertiesFileTests.java
  18. 34
      spring-test/src/test/java/org/springframework/test/context/env/repeatable/MetaFileTestProperty.java
  19. 3
      spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java
  20. 7
      spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java
  21. 9
      spring-test/src/test/java/org/springframework/test/context/web/AnnotationConfigWebContextLoaderTests.java
  22. 7
      spring-test/src/test/java/org/springframework/test/context/web/GenericXmlWebContextLoaderTests.java
  23. 5
      spring-test/src/test/resources/org/springframework/test/context/env/repeatable/local.yaml
  24. 7
      spring-test/src/test/resources/org/springframework/test/context/env/test-properties.yaml

13
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 @@ -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 @@ -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 @@ -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:

1
spring-test/spring-test.gradle

@ -85,6 +85,7 @@ dependencies { @@ -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

102
spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -91,6 +93,8 @@ public class MergedContextConfiguration implements Serializable {
private final String[] activeProfiles;
private final List<PropertySourceDescriptor> propertySourceDescriptors;
private final String[] propertySourceLocations;
private final String[] propertySourceProperties;
@ -151,7 +155,7 @@ public class MergedContextConfiguration implements Serializable { @@ -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 { @@ -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 { @@ -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<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses,
@Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations,
@ -233,10 +241,13 @@ public class MergedContextConfiguration implements Serializable { @@ -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<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses,
@Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations,
@ -244,12 +255,52 @@ public class MergedContextConfiguration implements Serializable { @@ -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.
* <p>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<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses,
@Nullable String[] activeProfiles, List<PropertySourceDescriptor> propertySourceDescriptors,
@Nullable String[] propertySourceProperties, @Nullable Set<ContextCustomizer> 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 { @@ -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}.
* <p>Properties will be loaded into the {@code Environment}'s set of
* {@code PropertySources}.
* @since 6.1
* @see TestPropertySource#locations
* @see TestPropertySource#factory
*/
public List<PropertySourceDescriptor> 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}.
* <p>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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}

27
spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -113,9 +114,10 @@ public @interface TestPropertySource {
* will be added to the enclosing {@code Environment} as its own property
* source, in the order declared.
* <h4>Supported File Formats</h4>
* <p>Both traditional and XML-based properties file formats are supported
* &mdash; for example, {@code "classpath:/com/example/test.properties"}
* or {@code "file:/path/to/file.xml"}.
* <p>By default, both traditional and XML-based properties file formats are
* supported &mdash; 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}.
* <h4>Path Resource Semantics</h4>
* <p>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 { @@ -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. <code>*&#42;/*.properties</code>)
* are not permitted: each location must evaluate to exactly one
* {@code .properties} or {@code .xml} resource. Property placeholders
* in paths (i.e., <code>${...}</code>) will be
* are not permitted: each location must evaluate to exactly one properties
* resource. Property placeholders in paths (i.e., <code>${...}</code>) will be
* {@linkplain org.springframework.core.env.Environment#resolveRequiredPlaceholders(String) resolved}
* against the {@code Environment}.
* <h4>Default Properties File Detection</h4>
@ -144,6 +145,7 @@ public @interface TestPropertySource { @@ -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 { @@ -278,4 +280,15 @@ public @interface TestPropertySource {
*/
boolean inheritProperties() default true;
/**
* Specify a custom {@link PropertySourceFactory}, if any.
* <p>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<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
}

3
spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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();

6
spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java

@ -127,15 +127,15 @@ public abstract class AbstractContextLoader implements SmartContextLoader { @@ -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);
}

2
spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java

@ -368,7 +368,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot @@ -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);

36
spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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; @@ -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<PropertySourceDescriptor> descriptors;
private final String[] properties;
@ -53,25 +55,25 @@ class MergedTestPropertySources { @@ -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<PropertySourceDescriptor> 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<PropertySourceDescriptor> getPropertySourceDescriptors() {
return this.descriptors;
}
/**
@ -84,8 +86,8 @@ class MergedTestPropertySources { @@ -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 { @@ -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 { @@ -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 { @@ -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();
}

85
spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -61,7 +64,7 @@ class TestPropertySourceAttributes {
private final MergedAnnotation<?> rootAnnotation;
private final List<String> locations = new ArrayList<>();
private final List<PropertySourceDescriptor> descriptors = new ArrayList<>();
private final boolean inheritLocations;
@ -70,12 +73,13 @@ class TestPropertySourceAttributes { @@ -70,12 +73,13 @@ class TestPropertySourceAttributes {
private final boolean inheritProperties;
TestPropertySourceAttributes(MergedAnnotation<TestPropertySource> 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<TestPropertySource> 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 { @@ -112,29 +116,52 @@ class TestPropertySourceAttributes {
attributeName));
}
private void addPropertiesAndLocationsFrom(MergedAnnotation<TestPropertySource> mergedAnnotation) {
@SuppressWarnings("unchecked")
private void addPropertiesAndLocationsFrom(MergedAnnotation<TestPropertySource> 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<? extends PropertySourceFactory> factoryClass =
(Class<? extends PropertySourceFactory>) 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<PropertySourceDescriptor> 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.
* <p>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<PropertySourceDescriptor> list, List<PropertySourceDescriptor> 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 { @@ -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}.
* <p>Note: The returned value may represent a <em>detected default</em>
* or merged locations that do not match the original value declared via a
* single {@code @TestPropertySource} annotation.
* @return the resource locations; potentially <em>empty</em>
* @return the resource location descriptors; potentially <em>empty</em>
* @see TestPropertySource#value
* @see TestPropertySource#locations
* @see TestPropertySource#factory
*/
String[] getLocations() {
return StringUtils.toStringArray(this.locations);
List<PropertySourceDescriptor> getPropertySourceDescriptors() {
return this.descriptors;
}
/**
@ -220,7 +249,7 @@ class TestPropertySourceAttributes { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -279,4 +308,12 @@ class TestPropertySourceAttributes {
return (Class<?>) source;
}
/**
* Determine if the supplied list contains no descriptor with locations.
*/
private static boolean hasNoLocations(List<PropertySourceDescriptor> descriptors) {
return descriptors.stream().map(PropertySourceDescriptor::locations)
.flatMap(List::stream).findAny().isEmpty();
}
}

114
spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -139,20 +145,18 @@ public abstract class TestPropertySourceUtils {
return duplicationDetected;
}
private static String[] mergeLocations(List<TestPropertySourceAttributes> attributesList) {
List<String> locations = new ArrayList<>();
private static List<PropertySourceDescriptor> mergeLocations(List<TestPropertySourceAttributes> attributesList) {
List<PropertySourceDescriptor> 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<TestPropertySourceAttributes> attributesList) {
@ -181,9 +185,10 @@ public abstract class TestPropertySourceUtils { @@ -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 { @@ -197,7 +202,8 @@ public abstract class TestPropertySourceUtils {
* <p>Property placeholders in resource locations (i.e., <code>${...}</code>)
* will be {@linkplain Environment#resolveRequiredPlaceholders(String) resolved}
* against the {@code Environment}.
* <p>Each properties file will be converted to a {@link ResourcePropertySource}
* <p>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 { @@ -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}.
* <p>Property placeholders in resource locations (i.e., <code>${...}</code>)
* will be {@linkplain Environment#resolveRequiredPlaceholders(String) resolved}
* against the {@code Environment}.
* <p>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<PropertySourceDescriptor> 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}.
* <p>Property placeholders in resource locations (i.e., <code>${...}</code>)
* will be {@linkplain Environment#resolveRequiredPlaceholders(String) resolved}
* against the {@code Environment}.
* <p>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<PropertySourceDescriptor> 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<? extends PropertySourceFactory> 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) {

60
spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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 { @@ -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<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses,
@Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, @Nullable String[] propertySourceProperties,
@ -135,14 +140,54 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { @@ -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<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses,
@Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, @Nullable String[] propertySourceProperties,
@Nullable Set<ContextCustomizer> 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.
* <p>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<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses,
@Nullable String[] activeProfiles,
List<PropertySourceDescriptor> propertySourceDescriptors, @Nullable String[] propertySourceProperties,
@Nullable Set<ContextCustomizer> 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 { @@ -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 { @@ -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 { @@ -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())

9
spring-test/src/test/java/org/springframework/test/context/MergedContextConfigurationTests.java

@ -18,6 +18,7 @@ package org.springframework.test.context; @@ -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 { @@ -409,9 +410,9 @@ class MergedContextConfigurationTests {
void equalsWithSameContextCustomizers() {
Set<ContextCustomizer> 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 { @@ -424,9 +425,9 @@ class MergedContextConfigurationTests {
Set<ContextCustomizer> 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);
}

62
spring-test/src/test/java/org/springframework/test/context/env/YamlPropertySourceFactory.java vendored

@ -0,0 +1,62 @@ @@ -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;
}
}

39
spring-test/src/test/java/org/springframework/test/context/env/YamlTestProperties.java vendored

@ -0,0 +1,39 @@ @@ -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();
}

59
spring-test/src/test/java/org/springframework/test/context/env/YamlTestPropertySourceTests.java vendored

@ -0,0 +1,59 @@ @@ -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 */
}
}

19
spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalPropertiesFileAndMetaPropertiesFileTests.java vendored

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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 @@ -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 {
}
}

43
spring-test/src/test/java/org/springframework/test/context/env/repeatable/LocalYamlFileAndMetaPropertiesFileTests.java vendored

@ -0,0 +1,43 @@ @@ -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");
}
}

34
spring-test/src/test/java/org/springframework/test/context/env/repeatable/MetaFileTestProperty.java vendored

@ -0,0 +1,34 @@ @@ -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 {
}

3
spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java

@ -1,5 +1,5 @@ @@ -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 @@ -187,6 +187,7 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
assertMergedConfigForLocationPaths(RelativeFooXmlLocation.class);
}
@SuppressWarnings("deprecation")
private void assertMergedConfigForLocationPaths(Class<?> testClass) {
MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass);

7
spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java

@ -18,7 +18,9 @@ package org.springframework.test.context.support; @@ -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; @@ -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 { @@ -295,7 +298,9 @@ class TestPropertySourceUtilsTests {
MergedTestPropertySources mergedPropertySources = buildMergedTestPropertySources(testClass);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(mergedPropertySources).isNotNull();
softly.assertThat(mergedPropertySources.getLocations()).isEqualTo(expectedLocations);
Stream<String> locations = mergedPropertySources.getPropertySourceDescriptors().stream()
.map(PropertySourceDescriptor::locations).flatMap(List::stream);
softly.assertThat(locations).containsExactly(expectedLocations);
softly.assertThat(mergedPropertySources.getProperties()).isEqualTo(expectedProperties);
});
}

9
spring-test/src/test/java/org/springframework/test/context/web/AnnotationConfigWebContextLoaderTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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");
}
}

7
spring-test/src/test/java/org/springframework/test/context/web/GenericXmlWebContextLoaderTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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");
}
}

5
spring-test/src/test/resources/org/springframework/test/context/env/repeatable/local.yaml vendored

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
key1: local file
environments:
dev:
url: https://dev.example.com
name: Developer Setup

7
spring-test/src/test/resources/org/springframework/test/context/env/test-properties.yaml vendored

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
environments:
dev:
url: https://dev.example.com
name: Developer Setup
prod:
url: https://prod.example.com
name: My Cool App
Loading…
Cancel
Save