|
5 | 5 | import java.io.IOException; |
6 | 6 | import java.nio.file.Files; |
7 | 7 | import java.nio.file.Path; |
| 8 | +import java.util.ArrayList; |
8 | 9 | import java.util.Collection; |
9 | 10 | import java.util.Collections; |
| 11 | +import java.util.LinkedHashMap; |
10 | 12 | import java.util.List; |
| 13 | +import java.util.Map; |
11 | 14 | import java.util.Properties; |
12 | 15 |
|
13 | 16 | import org.junit.jupiter.api.Test; |
|
18 | 21 | import io.quarkus.bootstrap.model.ApplicationModelBuilder; |
19 | 22 | import io.quarkus.bootstrap.model.ExtensionDevModeConfig; |
20 | 23 | import io.quarkus.bootstrap.model.JvmOption; |
| 24 | +import io.quarkus.bootstrap.model.MappableCollectionFactory; |
21 | 25 | import io.quarkus.bootstrap.model.PlatformImports; |
22 | 26 | import io.quarkus.maven.dependency.ArtifactKey; |
23 | 27 | import io.quarkus.maven.dependency.Dependency; |
@@ -171,6 +175,105 @@ void testSerializationWithDirectDependencies() throws IOException { |
171 | 175 | }); |
172 | 176 | } |
173 | 177 |
|
| 178 | + @Test |
| 179 | + void testReloadableWorkspaceModulesSerializedInDeterministicOrder() throws IOException { |
| 180 | + // Add the reloadable workspace modules in a deliberately non-sorted order |
| 181 | + ApplicationModel model = createBasicApplicationModelBuilder() |
| 182 | + .addReloadableWorkspaceModule(ArtifactKey.of("org.example", "z-module")) |
| 183 | + .addReloadableWorkspaceModule(ArtifactKey.of("org.example", "a-module")) |
| 184 | + .addReloadableWorkspaceModule(ArtifactKey.of("org.example", "m-module")) |
| 185 | + .addReloadableWorkspaceModule(ArtifactKey.of("org.example", "1-module")) |
| 186 | + .build(); |
| 187 | + |
| 188 | + Path serializedFile = tempDir.resolve("app-model-local-projects.json"); |
| 189 | + ApplicationModelSerializer.serialize(model, serializedFile); |
| 190 | + |
| 191 | + // local-projects must be serialized in natural (sorted) order regardless of insertion |
| 192 | + // order so the output is byte-stable across JVMs and doesn't bust the Gradle build cache |
| 193 | + String json = Files.readString(serializedFile); |
| 194 | + assertThat(json.indexOf("1-module")).isLessThan(json.indexOf("a-module")); |
| 195 | + assertThat(json.indexOf("a-module")).isLessThan(json.indexOf("m-module")); |
| 196 | + assertThat(json.indexOf("m-module")).isLessThan(json.indexOf("z-module")); |
| 197 | + } |
| 198 | + |
| 199 | + @Test |
| 200 | + void testRemovedResourcesSerializedInDeterministicOrder() throws IOException { |
| 201 | + // Populate removed resources through the extension-descriptor path, which stores each |
| 202 | + // artifact's resources as a per-JVM-salted Set.of(value.split(",")) -- the actual source of |
| 203 | + // nondeterminism the value sort defends against (see ApplicationModelBuilder#addRemovedResources). |
| 204 | + ApplicationModelBuilder builder = createBasicApplicationModelBuilder(); |
| 205 | + Properties props = new Properties(); |
| 206 | + props.setProperty(ApplicationModelBuilder.REMOVED_RESOURCES_DOT + "org.example:lib-a", |
| 207 | + "c-one.txt,a-one.txt,b-one.txt"); |
| 208 | + props.setProperty(ApplicationModelBuilder.REMOVED_RESOURCES_DOT + "org.example:lib-b", |
| 209 | + "z-two.txt,x-two.txt,y-two.txt"); |
| 210 | + builder.handleExtensionProperties(props, ArtifactKey.ga("io.quarkus", "some-extension")); |
| 211 | + ApplicationModel model = builder.build(); |
| 212 | + |
| 213 | + Path serializedFile = tempDir.resolve("app-model-removed-resources.json"); |
| 214 | + ApplicationModelSerializer.serialize(model, serializedFile); |
| 215 | + |
| 216 | + // The resources of each artifact must be serialized in natural (sorted) order regardless of the |
| 217 | + // salted Set.of iteration order, so the output is byte-stable across JVMs and does not bust the |
| 218 | + // Gradle build cache. (The map keys serialize in HashMap order, which is already JVM-stable.) |
| 219 | + String json = Files.readString(serializedFile); |
| 220 | + assertThat(json.indexOf("a-one.txt")).isLessThan(json.indexOf("b-one.txt")); |
| 221 | + assertThat(json.indexOf("b-one.txt")).isLessThan(json.indexOf("c-one.txt")); |
| 222 | + assertThat(json.indexOf("x-two.txt")).isLessThan(json.indexOf("y-two.txt")); |
| 223 | + assertThat(json.indexOf("y-two.txt")).isLessThan(json.indexOf("z-two.txt")); |
| 224 | + } |
| 225 | + |
| 226 | + @Test |
| 227 | + void testRemovedResourceKeysMappedInSortedOrder() { |
| 228 | + // Add removed resources for several artifacts in a deliberately non-sorted key order |
| 229 | + List<ArtifactKey> keys = List.of( |
| 230 | + ArtifactKey.of("org.example", "lib-c"), |
| 231 | + ArtifactKey.of("org.example", "lib-a"), |
| 232 | + ArtifactKey.of("org.example", "lib-d"), |
| 233 | + ArtifactKey.of("org.example", "lib-b")); |
| 234 | + ApplicationModelBuilder builder = createBasicApplicationModelBuilder(); |
| 235 | + keys.forEach(key -> builder.addRemovedResources(key, List.of("r.txt"))); |
| 236 | + ApplicationModel model = builder.build(); |
| 237 | + |
| 238 | + // The production JSON writer is HashMap-backed, so the serialized excluded-resources keys come |
| 239 | + // out in (JVM-stable) hash order, which we cannot assert as sorted. But asMap() is factory- |
| 240 | + // parameterized: mapping with an insertion-order-preserving factory exposes the order in which |
| 241 | + // asMap() emits the keys. That insertion order is what Map.Entry.comparingByKey() pins, and what |
| 242 | + // keeps the serialized output stable across JVMs (getRemovedResources() is a salted Map.copyOf). |
| 243 | + MappableCollectionFactory orderedFactory = new MappableCollectionFactory() { |
| 244 | + @Override |
| 245 | + public Map<String, Object> newMap() { |
| 246 | + return new LinkedHashMap<>(); |
| 247 | + } |
| 248 | + |
| 249 | + @Override |
| 250 | + public Map<String, Object> newMap(int initialCapacity) { |
| 251 | + return new LinkedHashMap<>(initialCapacity); |
| 252 | + } |
| 253 | + |
| 254 | + @Override |
| 255 | + public Collection<Object> newCollection() { |
| 256 | + return new ArrayList<>(); |
| 257 | + } |
| 258 | + |
| 259 | + @Override |
| 260 | + public Collection<Object> newCollection(int initialCapacity) { |
| 261 | + return new ArrayList<>(initialCapacity); |
| 262 | + } |
| 263 | + }; |
| 264 | + |
| 265 | + Map<String, Object> mapped = model.asMap(orderedFactory); |
| 266 | + @SuppressWarnings("unchecked") |
| 267 | + Map<String, Object> excludedResources = (Map<String, Object>) mapped |
| 268 | + .get(BootstrapConstants.MAPPABLE_EXCLUDED_RESOURCES); |
| 269 | + |
| 270 | + // asMap() must emit the keys in ArtifactKey natural order (what Map.Entry.comparingByKey uses), |
| 271 | + // rendered with the serializer's own toString -- compare against the keys' sorted order rather |
| 272 | + // than hardcoding the GACT string format. |
| 273 | + List<String> expectedKeyOrder = keys.stream().sorted().map(ArtifactKey::toString).toList(); |
| 274 | + assertThat(excludedResources.keySet()).containsExactlyElementsOf(expectedKeyOrder); |
| 275 | + } |
| 276 | + |
174 | 277 | @Test |
175 | 278 | void testSerializationWithExtensionDevModeConfig() throws IOException { |
176 | 279 | ApplicationModelBuilder builder = createBasicApplicationModelBuilder(); |
|
0 commit comments