junit-team/junit5 · error · PreconditionViolationException

TestPlan must only be executed once

Error message

TestPlan must only be executed once

What it means

InternalTestPlan.markStarted uses an AtomicBoolean compareAndSet(false, true); the second caller observes true and throws PreconditionViolationException. A TestPlan is a single-use, stateful object tied to one execution; executing it twice would corrupt listener/state tracking.

Solutions

  1. Rediscover a fresh TestPlan for each execution (call discover() again, or use execute(LauncherDiscoveryRequest) which always starts fresh).
  2. Track whether a plan has been executed in your code and do not reuse it.
  3. For re-run-on-failure, open a new session and rediscover rather than reusing the plan.

Example fix

// before
TestPlan plan = launcher.discover(request);
launcher.execute(plan, listener);
launcher.execute(plan, listener); // throws: already started
// after
launcher.execute(request, listener);
launcher.execute(request, listener); // fresh execution each time
Defensive patterns

Strategy: validation

Validate before calling

// Track whether the plan was started; rediscover for each run
AtomicBoolean started = new AtomicBoolean();
if (!started.compareAndSet(false, true)) {
    plan = launcher.discover(request); // fresh plan for the retry
}

Try / catch

try {
    launcher.execute(plan, listener);
} catch (PreconditionViolationException e) {
    if (e.getMessage().contains("only be executed once")) {
        plan = launcher.discover(originalRequest); // rediscover
        launcher.execute(plan, listener);
    }
}

Prevention

When it happens

Trigger: Calling Launcher.execute(testPlan) twice with the same InternalTestPlan, or invoking markStarted() a second time. The CAS guards exactly-once execution semantics.

Common situations: Reusing a discovered TestPlan across two execute() calls; a listener/wrapper that retries execution on the same plan; test harness code that re-runs a plan for re-run-on-failure features without rediscovering; building a custom launcher flow that re-submits a plan.

Related errors


AI-assisted analysis of junit-team/junit5@f070c699a0 (2026-08-11). Data as JSON: /api/errors/ca15ab02d80a02b4. Report an issue: GitHub.

Appendix: source

Thrown at junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java:47

	private final LauncherDiscoveryResult discoveryResult;
	private final TestPlan delegate;

	static InternalTestPlan from(LauncherDiscoveryResult discoveryResult) {
		TestPlan delegate = TestPlan.from(discoveryResult.containsCriticalIssuesOrContainsTests(),
			discoveryResult.getEngineTestDescriptors(), discoveryResult.getConfigurationParameters(),
			discoveryResult.getOutputDirectoryCreator());
		return new InternalTestPlan(discoveryResult, delegate);
	}

	private InternalTestPlan(LauncherDiscoveryResult discoveryResult, TestPlan delegate) {
		super(delegate.containsTests(), delegate.getConfigurationParameters(), delegate.getOutputDirectoryCreator());
		this.discoveryResult = discoveryResult;
		this.delegate = delegate;
	}

	void markStarted() {
		if (!executionStarted.compareAndSet(false, true)) {
			throw new PreconditionViolationException("TestPlan must only be executed once");
		}
	}

	LauncherDiscoveryResult getDiscoveryResult() {
		return discoveryResult;
	}

	public TestPlan getDelegate() {
		return delegate;
	}

	@Override
	public void addInternal(TestIdentifier testIdentifier) {
		delegate.addInternal(testIdentifier);
	}

	@Override
	public void removeInternal(UniqueId uniqueId) {

View on GitHub (pinned to f070c699a0)