junit-team/junit5 · error · PreconditionViolationException

TestPlan must only be executed once

Error message

TestPlan must only be executed once

What it means

Thrown by InternalTestPlan.markStarted() when an AtomicBoolean compareAndSet fails because execution already started. An InternalTestPlan is single-use: it can be passed to Launcher.execute(TestPlan, ...) only once. Subsequent attempts raise PreconditionViolationException.

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 956246301e)

Solutions

  1. Rediscover the TestPlan via launcher.discover(request) before each execute(TestPlan, ...).
  2. Use the execute(LauncherDiscoveryRequest, ...) overload which creates a fresh plan internally.
  3. Ensure your retry/rerun logic builds a new plan rather than reusing the old one.

Example fix

// before
TestPlan plan = launcher.discover(request);
launcher.execute(plan, listener);
launcher.execute(plan, listener); // throws

// after
launcher.execute(request, listener);
// or rediscover:
TestPlan plan2 = launcher.discover(request);
launcher.execute(plan2, listener);
Defensive patterns

Strategy: validation

Validate before calling

// Track execution yourself before relying on InternalTestPlan's guard
AtomicBoolean executed = new AtomicBoolean();
if (!executed.compareAndSet(false, true)) {
    throw new IllegalStateException("plan already executed");
}
launcher.execute(plan, listener);

Type guard

static boolean isExecutedOnce(InternalTestPlan plan) {
    // InternalTestPlan is package-private; expose via a wrapper tracking markStarted()
    return plan != null; // replaced by your own AtomicBoolean in caller code
}

Try / catch

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

Prevention

When it happens

Trigger: Calling launcher.execute(testPlan, listeners) twice with the same TestPlan instance. The first call flips executionStarted to true via markStarted(); the second fails the CAS.

Common situations: Caching a discovered TestPlan and re-executing it (e.g. retry logic, or running the same plan in multiple threads). Reusing a TestPlan across re-runs in a custom runner. Misranged loops that execute the same plan object repeatedly.

Related errors


AI-assisted analysis of junit-team/junit5@956246301e (2026-08-04). Data as JSON: /data/errors/ca15ab02d80a02b4.json. Report an issue: GitHub.