From b43972bb54b85cc95c62cc942785116bbb2190dc Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 16 Aug 2023 13:48:25 +0200 Subject: [PATCH] Preserve property ordering for text blocks in @TestPropertySource See gh-31053 --- .../support/TestPropertySourceUtils.java | 48 ++++++++++++------- ...sWithTextBlockTestPropertySourceTests.java | 8 +--- 2 files changed, 33 insertions(+), 23 deletions(-) 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 46460041fe3..01bebbc5ac8 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 @@ -373,12 +373,11 @@ public abstract class TestPropertySourceUtils { *

Parsing of the key-value pairs is achieved by converting all supplied * strings into virtual properties files in memory and delegating to * {@link Properties#load(java.io.Reader)} to parse each virtual file. - *

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 — for example, when supplied by - * a user via a text block — the ordering of property names for - * those particular key-value pairs cannot be guaranteed in the returned map. + *

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 — for example, when supplied by a user + * via a text block. *

For a full discussion of inlined properties, 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 convertInlinedPropertiesToMap(String... inlinedProperties) { Assert.notNull(inlinedProperties, "'inlinedProperties' must not be null"); - Map 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 List>> findRepeatableAnnotations( @@ -456,4 +450,24 @@ public abstract class TestPropertySourceUtils { return Comparator.> 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 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); + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesWithTextBlockTestPropertySourceTests.java b/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesWithTextBlockTestPropertySourceTests.java index 95bc931bcd4..ef35896b03a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesWithTextBlockTestPropertySourceTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesWithTextBlockTestPropertySourceTests.java @@ -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"); }