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 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..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)); - // @formatter:on + var retainedEngines = new LinkedHashMap<>(this.testEngineResults); + retainedEngines.entrySet().removeIf(entry -> !predicate.test(entry.getValue().getRootDescriptor())); + return retainedEngines; } static class EngineResultInfo { 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 { + } + +}