{"id":"ca15ab02d80a02b4","repo":"junit-team/junit5","slug":"testplan-must-only-be-executed-once","errorCode":null,"errorMessage":"TestPlan must only be executed once","messagePattern":"TestPlan must only be executed once","errorType":"exception","errorClass":"PreconditionViolationException","httpStatus":null,"severity":"error","filePath":"junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java","lineNumber":47,"sourceCode":"\tprivate final LauncherDiscoveryResult discoveryResult;\n\tprivate final TestPlan delegate;\n\n\tstatic InternalTestPlan from(LauncherDiscoveryResult discoveryResult) {\n\t\tTestPlan delegate = TestPlan.from(discoveryResult.containsCriticalIssuesOrContainsTests(),\n\t\t\tdiscoveryResult.getEngineTestDescriptors(), discoveryResult.getConfigurationParameters(),\n\t\t\tdiscoveryResult.getOutputDirectoryCreator());\n\t\treturn new InternalTestPlan(discoveryResult, delegate);\n\t}\n\n\tprivate InternalTestPlan(LauncherDiscoveryResult discoveryResult, TestPlan delegate) {\n\t\tsuper(delegate.containsTests(), delegate.getConfigurationParameters(), delegate.getOutputDirectoryCreator());\n\t\tthis.discoveryResult = discoveryResult;\n\t\tthis.delegate = delegate;\n\t}\n\n\tvoid markStarted() {\n\t\tif (!executionStarted.compareAndSet(false, true)) {\n\t\t\tthrow new PreconditionViolationException(\"TestPlan must only be executed once\");\n\t\t}\n\t}\n\n\tLauncherDiscoveryResult getDiscoveryResult() {\n\t\treturn discoveryResult;\n\t}\n\n\tpublic TestPlan getDelegate() {\n\t\treturn delegate;\n\t}\n\n\t@Override\n\tpublic void addInternal(TestIdentifier testIdentifier) {\n\t\tdelegate.addInternal(testIdentifier);\n\t}\n\n\t@Override\n\tpublic void removeInternal(UniqueId uniqueId) {","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/junit-team/junit5/blob/956246301e03d757539f7b76dd5a91f298637563/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java#L29-L65","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nTestPlan plan = launcher.discover(request);\nlauncher.execute(plan, listener);\nlauncher.execute(plan, listener); // throws\n\n// after\nlauncher.execute(request, listener);\n// or rediscover:\nTestPlan plan2 = launcher.discover(request);\nlauncher.execute(plan2, listener);","handlingStrategy":"validation","validationCode":"// Track execution yourself before relying on InternalTestPlan's guard\nAtomicBoolean executed = new AtomicBoolean();\nif (!executed.compareAndSet(false, true)) {\n    throw new IllegalStateException(\"plan already executed\");\n}\nlauncher.execute(plan, listener);","typeGuard":"static boolean isExecutedOnce(InternalTestPlan plan) {\n    // InternalTestPlan is package-private; expose via a wrapper tracking markStarted()\n    return plan != null; // replaced by your own AtomicBoolean in caller code\n}","tryCatchPattern":"try {\n    launcher.execute(plan, listener);\n} catch (PreconditionViolationException e) {\n    if (e.getMessage().contains(\"only be executed once\")) {\n        plan = launcher.discover(request); // rediscover, then retry\n        launcher.execute(plan, listener);\n    } else { throw e; }\n}","preventionTips":["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."],"tags":["execution","test-plan","lifecycle","junit-platform"],"analyzedSha":"956246301e03d757539f7b76dd5a91f298637563","analyzedAt":"2026-08-04T19:25:17.049Z","schemaVersion":2}