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
- Rediscover the TestPlan via launcher.discover(request) before each execute(TestPlan, ...).
- Use the execute(LauncherDiscoveryRequest, ...) overload which creates a fresh plan internally.
- 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
- Never reuse a TestPlan across multiple execute(TestPlan, ...) calls.
- Prefer execute(LauncherDiscoveryRequest, ...) which builds a fresh plan each time.
- In retry/rerun logic, rediscover before re-executing.
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
- Error executing tests for engine ${getId()}
- A NamespacedHierarchicalStore cannot be modified or queried
- Launcher session has already been closed
- Failed to close XML events file
- Cannot override the standard style 'NONE'
AI-assisted analysis of junit-team/junit5@956246301e (2026-08-04).
Data as JSON: /data/errors/ca15ab02d80a02b4.json.
Report an issue: GitHub.