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
- Rediscover a fresh TestPlan for each execution (call discover() again, or use execute(LauncherDiscoveryRequest) which always starts fresh).
- Track whether a plan has been executed in your code and do not reuse it.
- 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
- Rediscover a new TestPlan for each execution rather than reusing.
- Prefer execute(LauncherDiscoveryRequest) which is inherently single-use.
- Track plan state in your harness to avoid double execution.
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
- Error executing tests for engine
- Filter [ ] must implement , , or .
- Launcher session has already been closed
- temp directory must be a directory
- thread mode must not be INFERRED
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)