Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,21 @@ public class LauncherConstants {
@API(status = EXPERIMENTAL, since = "6.0")
public static final String DISCOVERY_ISSUE_FAILURE_PHASE_PROPERTY_NAME = "junit.platform.discovery.issue.failure.phase";

/**
* Property name used to enable the <em>experimental</em> memory cleanup
* mode.
*
* <p>Supported values are {@code true} or {@code false}.
*
* <p>If enabled, the Launcher removes finished or skipped tests and their
* children from the test plan right away to reduce memory consumption,
* particularly in the presence of many dynamically reported tests.
*
* @since 6.1
*/
@API(status = EXPERIMENTAL, since = "6.1")
public static final String MEMORY_CLEANUP_ENABLED_PROPERTY_NAME = "junit.platform.execution.memory.cleanup.enabled";

private LauncherConstants() {
/* no-op */
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ public void addInternal(TestIdentifier testIdentifier) {
directChildren.add(testIdentifier);
}

@API(status = INTERNAL, since = "6.1")
public void removeInternal(UniqueId uniqueId) {
Preconditions.notNull(uniqueId, "uniqueId must not be null");
allIdentifiers.remove(uniqueId);
roots.removeIf(testIdentifier -> testIdentifier.getUniqueIdObject().equals(uniqueId));
children.remove(uniqueId);
Comment thread
marcphilipp marked this conversation as resolved.
Outdated
}

/**
* Get the root {@link TestIdentifier TestIdentifiers} for this test plan.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.support.store.Namespace;
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
import org.junit.platform.launcher.LauncherConstants;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
Expand Down Expand Up @@ -141,11 +142,21 @@ private static EngineExecutionListener buildEngineExecutionListener(
EngineExecutionListener parentEngineExecutionListener, TestExecutionListener testExecutionListener,
TestPlan testPlan) {
ListenerRegistry<EngineExecutionListener> engineExecutionListenerRegistry = forEngineExecutionListeners();
engineExecutionListenerRegistry.add(new ExecutionListenerAdapter(testPlan, testExecutionListener));
EngineExecutionListener listener = new ExecutionListenerAdapter(testPlan, testExecutionListener);
if (isMemoryCleanupEnabled(testPlan)) {
listener = new MemoryCleanupListener(listener, testPlan);
}
engineExecutionListenerRegistry.add(listener);
engineExecutionListenerRegistry.add(parentEngineExecutionListener);
return engineExecutionListenerRegistry.getCompositeListener();
}

private static Boolean isMemoryCleanupEnabled(TestPlan testPlan) {
return testPlan.getConfigurationParameters() //
.getBoolean(LauncherConstants.MEMORY_CLEANUP_ENABLED_PROPERTY_NAME) //
.orElse(false);
}

private void withInterceptedStreams(ConfigurationParameters configurationParameters,
ListenerRegistry<TestExecutionListener> listenerRegistry, Consumer<TestExecutionListener> action) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.function.Predicate;

import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
Expand Down Expand Up @@ -61,6 +62,11 @@ public void addInternal(TestIdentifier testIdentifier) {
delegate.addInternal(testIdentifier);
}

@Override
public void removeInternal(UniqueId uniqueId) {
delegate.removeInternal(uniqueId);
}

@Override
public Set<TestIdentifier> getRoots() {
return delegate.getRoots();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2026 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 java.util.stream.Stream;

import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.launcher.TestPlan;

/**
* @since 6.1
*/
class MemoryCleanupListener extends DelegatingEngineExecutionListener {

private final TestPlan testPlan;

MemoryCleanupListener(EngineExecutionListener delegate, TestPlan testPlan) {
super(delegate);
this.testPlan = testPlan;
}

@Override
public void executionSkipped(TestDescriptor testDescriptor, String reason) {
super.executionSkipped(testDescriptor, reason);
cleanUp(testDescriptor);
}

@Override
public void executionFinished(TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) {
super.executionFinished(testDescriptor, testExecutionResult);
cleanUp(testDescriptor);
}

private void cleanUp(TestDescriptor testDescriptor) {
var ownUniqueId = Stream.of(testDescriptor.getUniqueId());
var descendantUniqueIds = testDescriptor.getDescendants().stream() //
.map(TestDescriptor::getUniqueId);
Stream.concat(ownUniqueId, descendantUniqueIds) //
.forEach(testPlan::removeInternal);
Comment thread
marcphilipp marked this conversation as resolved.
Outdated
if (!testDescriptor.isRoot()) {
testDescriptor.removeFromHierarchy();
}
}
}
Loading