Preserve property ordering for text blocks in @TestPropertySource

See gh-31053
This commit is contained in:
Sam Brannen 2023-08-16 13:48:25 +02:00
parent 17109b2402
commit b43972bb54
2 changed files with 33 additions and 23 deletions

View File

@ -373,12 +373,11 @@ public abstract class TestPropertySourceUtils {
* <p>Parsing of the key-value pairs is achieved by converting all supplied
* strings into <em>virtual</em> properties files in memory and delegating to
* {@link Properties#load(java.io.Reader)} to parse each virtual file.
* <p>Generally speaking, the ordering of property names will be preserved in
* the returned map, analogous to the order in which the key-value pairs are
* supplied to this method. However, if a single string contains multiple
* key-value pairs separated by newlines &mdash; for example, when supplied by
* a user via a <em>text block</em> &mdash; the ordering of property names for
* those particular key-value pairs cannot be guaranteed in the returned map.
* <p>The ordering of property names will be preserved in the returned map,
* analogous to the order in which the key-value pairs are supplied to this
* method. This also applies if a single string contains multiple key-value
* pairs separated by newlines &mdash; for example, when supplied by a user
* via a <em>text block</em>.
* <p>For a full discussion of <em>inlined properties</em>, consult the Javadoc
* for {@link TestPropertySource#properties}.
* @param inlinedProperties the inlined properties to convert; potentially empty
@ -390,26 +389,21 @@ public abstract class TestPropertySourceUtils {
*/
public static Map<String, Object> convertInlinedPropertiesToMap(String... inlinedProperties) {
Assert.notNull(inlinedProperties, "'inlinedProperties' must not be null");
Map<String, Object> map = new LinkedHashMap<>();
Properties props = new Properties();
SequencedProperties sequencedProperties = new SequencedProperties();
for (String pair : inlinedProperties) {
if (!StringUtils.hasText(pair)) {
for (String input : inlinedProperties) {
if (!StringUtils.hasText(input)) {
continue;
}
try {
props.load(new StringReader(pair));
sequencedProperties.load(new StringReader(input));
}
catch (Exception ex) {
throw new IllegalStateException("Failed to load test environment properties from [" + pair + "]", ex);
throw new IllegalStateException("Failed to load test environment properties from [" + input + "]", ex);
}
for (String name : props.stringPropertyNames()) {
map.put(name, props.getProperty(name));
}
props.clear();
}
return map;
return sequencedProperties.map;
}
private static <T extends Annotation> List<List<MergedAnnotation<T>>> findRepeatableAnnotations(
@ -456,4 +450,24 @@ public abstract class TestPropertySourceUtils {
return Comparator.<MergedAnnotation<A>> comparingInt(MergedAnnotation::getDistance).reversed();
}
/**
* Extension of {@link Properties} that mimics a {@code SequencedMap} by
* tracking all added properties in a {@link LinkedHashMap} that can be
* accessed directly via the {@code map} field.
* @since 6.1
*/
@SuppressWarnings("serial")
private static class SequencedProperties extends Properties {
final Map<String, Object> map = new LinkedHashMap<>();
@Override
public synchronized Object put(Object key, Object value) {
if (key instanceof String str) {
this.map.put(str, value);
}
return super.put(key, value);
}
}
}

View File

@ -78,16 +78,12 @@ class InlinedPropertiesWithTextBlockTestPropertySourceTests {
assertEnvironmentProperty(this.env, "key.value.3", "key:value");
}
/**
* Not necessarily preserved because the properties are all added at the
* same time.
*/
@Test
@SuppressWarnings("rawtypes")
void propertyNameOrderingIsNotNecessarilyPreservedInEnvironment() {
void propertyNameOrderingIsPreservedInEnvironment() {
EnumerablePropertySource eps = (EnumerablePropertySource) env.getPropertySources().get(
INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
assertThat(eps.getPropertyNames()).containsExactlyInAnyOrder("foo", "baz", "enigma", "x.y.z",
assertThat(eps.getPropertyNames()).containsExactly("foo", "baz", "enigma", "x.y.z",
"server.url", "key.value.1", "key.value.2", "key.value.3");
}