From 1074ea578c39681031661a26bf2cac36d43e062c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:14:37 +0200 Subject: [PATCH 1/3] Retain TestEngine order after pruning Prior to this commit, LauncherDiscoveryResult.retainEngines() stored the pruned LinkedHashMap content in a HashMap, which sometimes resulted in different iteration order for pruned test engines. To address that, this commit replaces the use of Collectors.toMap(Function, Function) -- which creates a HashMap -- with the use of Collectors.toMap(Function, Function, BinaryOperator, Supplier) -- which allows us to specify that the pruned test engines should be stored in a LinkedHashMap to retain the original iteration order. Fixes #4862 --- .../core/LauncherDiscoveryResult.java | 2 +- .../core/LauncherDiscoveryResultTests.java | 111 ++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherDiscoveryResultTests.java diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java index 6827cd0246a0..373d18807f86 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java @@ -100,7 +100,7 @@ private Map retainEngines(Predicate predicate.test(entry.getValue().getRootDescriptor())) - .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); // @formatter:on } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherDiscoveryResultTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherDiscoveryResultTests.java new file mode 100644 index 000000000000..b3545351bf6b --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherDiscoveryResultTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.launcher.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.launcher.core.LauncherDiscoveryResult.EngineResultInfo; + +/** + * Unit tests for {@link LauncherDiscoveryResult}. + */ +class LauncherDiscoveryResultTests { + + /** + * @see GitHub issue #4862 + */ + @Test + void withRetainedEnginesRetainsLinkedHashMapSemantics() { + TestEngine engine1 = new DummyEngine1(); + TestEngine engine2 = new DummyEngine2(); + TestEngine engine3 = new DummyEngine3(); + TestEngine engine4 = new DummyEngine4(); + + TestDescriptor rootDescriptor1 = mock(); + TestDescriptor rootDescriptor2 = mock(); + TestDescriptor rootDescriptor3 = mock(); + TestDescriptor rootDescriptor4 = mock(); + when(rootDescriptor1.isTest()).thenReturn(true); + when(rootDescriptor2.isTest()).thenReturn(false); + when(rootDescriptor3.isTest()).thenReturn(false); + when(rootDescriptor4.isTest()).thenReturn(true); + + EngineResultInfo engineResultInfo1 = mock(); + EngineResultInfo engineResultInfo2 = mock(); + EngineResultInfo engineResultInfo3 = mock(); + EngineResultInfo engineResultInfo4 = mock(); + when(engineResultInfo1.getRootDescriptor()).thenReturn(rootDescriptor1); + when(engineResultInfo2.getRootDescriptor()).thenReturn(rootDescriptor2); + when(engineResultInfo3.getRootDescriptor()).thenReturn(rootDescriptor3); + when(engineResultInfo4.getRootDescriptor()).thenReturn(rootDescriptor4); + + @SuppressWarnings("serial") + Map engineResults = new LinkedHashMap<>() { + { + put(engine1, engineResultInfo1); + put(engine2, engineResultInfo2); + put(engine3, engineResultInfo3); + put(engine4, engineResultInfo4); + } + }; + + assertThat(engineResults.keySet()).containsExactly(engine1, engine2, engine3, engine4); + + LauncherDiscoveryResult discoveryResult = new LauncherDiscoveryResult(engineResults, mock(), mock()); + assertThat(discoveryResult.getTestEngines()).containsExactly(engine1, engine2, engine3, engine4); + + LauncherDiscoveryResult withRetainedEngines = discoveryResult.withRetainedEngines(TestDescriptor::isTest); + + assertThat(withRetainedEngines.getTestEngines()).containsExactly(engine1, engine4); + } + + private static abstract class AbstractDummyEngine implements TestEngine { + + @Override + public String getId() { + return getClass().getSimpleName(); + } + + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + throw new UnsupportedOperationException("discover"); + } + + @Override + public void execute(ExecutionRequest request) { + throw new UnsupportedOperationException("execute"); + } + } + + private static class DummyEngine1 extends AbstractDummyEngine { + } + + private static class DummyEngine2 extends AbstractDummyEngine { + } + + private static class DummyEngine3 extends AbstractDummyEngine { + } + + private static class DummyEngine4 extends AbstractDummyEngine { + } + +} From 9a51176c4c4e4844d23ce392dc79136afde3eb64 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:47:40 +0200 Subject: [PATCH 2/3] Use entrySet().removeIf() instead of Stream API and toMap() See #4862 --- .../launcher/core/LauncherDiscoveryResult.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java index 373d18807f86..2510a43a5f5c 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java @@ -12,7 +12,6 @@ import static java.util.Collections.unmodifiableMap; import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toMap; import static org.apiguardian.api.API.Status.INTERNAL; import java.util.Collection; @@ -96,12 +95,9 @@ public LauncherDiscoveryResult withRetainedEngines(Predicate retainEngines(Predicate predicate) { - // @formatter:off - return this.testEngineResults.entrySet() - .stream() - .filter(entry -> predicate.test(entry.getValue().getRootDescriptor())) - .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); - // @formatter:on + var retainedEngines = new LinkedHashMap<>(this.testEngineResults); + retainedEngines.entrySet().removeIf(entry -> !predicate.test(entry.getValue().getRootDescriptor())); + return retainedEngines; } static class EngineResultInfo { From b0567d569c3d7ffa3817e245cc13fecba3f0f92d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:52:58 +0200 Subject: [PATCH 3/3] Document #4862 in the 6.0 RC2 Release Notes --- .../docs/asciidoc/release-notes/release-notes-6.0.0-RC2.adoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-RC2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-RC2.adoc index ae181dc4cbe0..36eb3a5d93d5 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-RC2.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-RC2.adoc @@ -24,7 +24,9 @@ guidance on upgrading from JUnit 5.x.y to 6.0.0. [[release-notes-6.0.0-RC2-junit-platform-bug-fixes]] ==== Bug Fixes -* ❓ +* The `Launcher` (specifically `LauncherDiscoveryResult`) now retains the original + `TestEngine` registration order after pruning test engines without tests, thereby + ensuring reliable test execution order of multiple test engines. [[release-notes-6.0.0-RC2-junit-platform-deprecations-and-breaking-changes]] ==== Deprecations and Breaking Changes