Loading plugins/org.etsi.mts.tdl.execution.java.codegen/src/org/etsi/mts/tdl/execution/java/codegen/JUnitTestGenerator.java +6 −4 Original line number Diff line number Diff line Loading @@ -1607,8 +1607,10 @@ public class JUnitTestGenerator extends Renderer { writeNotification((ExceptionalBehaviour) bl.eContainer(), true); lineComment("Disable while exceptional behaviour is executed"); line(COMPONENT_FIELD + ".disableExceptionalBehaviour(" + exceptionalBehaviourName + ");"); lineComment("Protect the handler body: suspend all active exceptional behaviours"); lineComment("(siblings, outer defaults/interrupts and this one) for its duration."); line("List<ExceptionalBehaviour> " + exceptionalBehaviourName + "_suspended = " + COMPONENT_FIELD + ".disableActiveExceptionalBehaviours();"); newLine(); append("try"); Loading Loading @@ -1636,8 +1638,8 @@ public class JUnitTestGenerator extends Renderer { append("finally"); blockOpen(); lineComment("Enable the exceptional behaviour again"); line(COMPONENT_FIELD + ".enableExceptionalBehaviour(" + exceptionalBehaviourName + ");"); lineComment("Re-enable the suspended exceptional behaviours"); line(COMPONENT_FIELD + ".enableExceptionalBehaviours(" + exceptionalBehaviourName + "_suspended);"); writeNotification((ExceptionalBehaviour) bl.eContainer(), false); blockClose(); Loading plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/rt/core/ReceiverHub.java +36 −12 Original line number Diff line number Diff line Loading @@ -29,9 +29,19 @@ public class ReceiverHub { // Per-thread batch state: each thread's entry is {batchSize, batchCount}. // Expectations from threads with incomplete batches are skipped by run(). private Map<Thread, int[]> batches = new HashMap<>(); // Per-thread suspended state: threads whose expectations matched are // suspended until they finish cleanup and call resume(). private Set<Thread> suspendedThreads = new HashSet<>(); /** * Shared per-round exclusivity token. Every expectable submitted as part of a * single alternative round (the alternatives, the active exceptionals and the * anyReceivers of one {@code executeAlternativesAndExceptionals} call) shares * one instance. As soon as any of them matches, {@link #run()} sets * {@link #resolved} so all remaining competitors of the same round are skipped * and never processed — realising the TDL "only one alternative Block is * executed" semantics at the instant of the match. */ public static class AlternativeRound { volatile boolean resolved; } public ReceiverHub(SystemAdapter systemAdapter, Connection connection, NamedElement testerComponent) { this.systemAdapter = systemAdapter; Loading Loading @@ -64,9 +74,13 @@ public class ReceiverHub { } } /** * Wakes the receiver loop. Retained for the generated code that calls * {@code TestControl.resumeReceiving()} after a round; round exclusivity is now * handled by {@link AlternativeRound}, so this only needs to nudge the loop. */ public void resume() { synchronized (expecting) { suspendedThreads.remove(Thread.currentThread()); expecting.notifyAll(); } } Loading @@ -74,9 +88,9 @@ public class ReceiverHub { private synchronized void run() { // Track tried expectables by identity rather than positional index, // since the expecting list is mutated concurrently by other threads. // Stale entries after resume are harmless — each hub.receive() call // creates a new Expectable instance with distinct identity, so stale // entries from previous cycles never match newly added expectables. // Stale entries are harmless — each hub.receive() call creates a new // Expectable instance with distinct identity, so stale entries from // previous cycles never match newly added expectables. Set<Expectable> tried = new HashSet<>(); while (!stopped) { Expectable currentlyExpecting = null; Loading @@ -85,7 +99,7 @@ public class ReceiverHub { while (currentlyExpecting == null) { for (Expectable e : expecting) if (!tried.contains(e) && !suspendedThreads.contains(e.ownerThread) && !(e.round != null && e.round.resolved) && !hasPendingBatch(e.ownerThread)) { currentlyExpecting = e; break; Loading Loading @@ -113,7 +127,10 @@ public class ReceiverHub { } } expecting.remove(currentlyExpecting); suspendedThreads.add(currentlyExpecting.ownerThread); // Resolve the round BEFORE notifying: the next loop iteration then // skips every remaining competitor of this round so none can match. if (currentlyExpecting.round != null) currentlyExpecting.round.resolved = true; synchronized (currentlyExpecting) { currentlyExpecting.received = data; currentlyExpecting.notifyAll(); Loading @@ -128,7 +145,8 @@ public class ReceiverHub { } catch (RuntimeException e) { currentlyExpecting.error = e; expecting.remove(currentlyExpecting); suspendedThreads.add(currentlyExpecting.ownerThread); if (currentlyExpecting.round != null) currentlyExpecting.round.resolved = true; synchronized (currentlyExpecting) { currentlyExpecting.notifyAll(); } Loading @@ -142,7 +160,11 @@ public class ReceiverHub { } public Data receive(Data expected, boolean ignoreUntil) { Expectable expectable = new Expectable(expected, ignoreUntil); return receive(expected, ignoreUntil, null); } public Data receive(Data expected, boolean ignoreUntil, AlternativeRound round) { Expectable expectable = new Expectable(expected, ignoreUntil, round); synchronized (expecting) { expecting.add(expectable); int[] batch = batches.get(Thread.currentThread()); Loading Loading @@ -193,11 +215,13 @@ public class ReceiverHub { boolean ignoreUnmatched = false; boolean anyReceiver = false; Thread ownerThread = Thread.currentThread(); AlternativeRound round; public Expectable(Data expected, boolean ignoreUnmatched) { public Expectable(Data expected, boolean ignoreUnmatched, AlternativeRound round) { this.expected = expected; this.ignoreUnmatched = ignoreUnmatched; this.anyReceiver = expected == null; this.round = round; } } } plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/rt/core/TestControl.java +58 −3 Original line number Diff line number Diff line Loading @@ -125,7 +125,7 @@ public class TestControl { @Override public ExecutionResult call() throws Exception { try { Data data = hub.receive(null, false); Data data = hub.receive(null, false, this.round); return data != null ? new InteractionResult(data) : null; } catch (RuntimeException e) { // Propagate SA errors through the behaviour execution path: Loading Loading @@ -201,6 +201,15 @@ public class TestControl { } public abstract class ExecutionCallable implements Callable<ExecutionResult> { /** * The exclusivity round this callable was submitted under, assigned by * {@link #executeAlternativesAndExceptionals(List)} before {@link #execute()}. * Propagated to the {@link ReceiverHub.Expectable} so a match in the round * disables every competitor. Volatile: written by the submitting thread, * read by the pool thread running {@link #call()}. */ public volatile ReceiverHub.AlternativeRound round; public Future<ExecutionResult> execute() { return completionService.get().submit(this); } Loading Loading @@ -283,18 +292,30 @@ public class TestControl { // Enable batches BEFORE submitting any callables hubCounts.forEach((hub, count) -> hub.enableBatch(count)); // One exclusivity round shared by every expectable submitted here: the // alternatives, the active exceptionals and the anyReceivers. When any of // them matches, the round is resolved and the rest are disabled at once. // The round is assigned BEFORE execute() so it is visible to the pool // thread that creates the Expectable (and before batch gating releases it). ReceiverHub.AlternativeRound round = new ReceiverHub.AlternativeRound(); // Submit alternatives List<Future<ExecutionResult>> altFutures = new ArrayList<>(); for (ExecutionCallable c : alternatives) { c.round = round; altFutures.add(c.execute()); } // Submit exceptionals (priority-ordered) and per-hub anyReceivers List<Future<ExecutionResult>> excFutures = new ArrayList<>(); for (ExceptionalBehaviour exc : enabledExcs) { exc.callable.round = round; excFutures.add(exc.execute()); } receiverHubs.values().forEach(hub -> excFutures.add(hub.getAnyReceiver().execute())); receiverHubs.values().forEach(hub -> { hub.getAnyReceiver().callable.round = round; excFutures.add(hub.getAnyReceiver().execute()); }); return new BatchedFutures(altFutures, excFutures, hubCounts.keySet()); } Loading Loading @@ -402,7 +423,7 @@ public class TestControl { } @Override public ExecutionResult call() throws Exception { Data data = getReceiver().receive(expected, isTrigger); Data data = getReceiver().receive(expected, isTrigger, this.round); return data != null ? new InteractionResult(data) : null; } }; Loading Loading @@ -480,6 +501,40 @@ public class TestControl { b.enabled = true; } /** * Disable ALL currently enabled exceptional behaviours and return the snapshot. * Used to make an exceptional handler's body a protected region: while it runs, * the other exceptionals that were active when it fired (siblings, outer * defaults/interrupts, and the handler itself) are suspended so none of them * can fire during the handler. The handler's own nested exceptionals are added * after this call and therefore stay active; the implicit per-hub anyReceivers * are not in this list and remain active. Restore with * {@link #enableExceptionalBehaviours(List)}. */ public List<ExceptionalBehaviour> disableActiveExceptionalBehaviours() { List<ExceptionalBehaviour> snapshot = new ArrayList<>(); List<ExceptionalBehaviour> excs = exceptionalBehaviours.get(); synchronized (excs) { for (ExceptionalBehaviour b : excs) { if (b.enabled) { b.enabled = false; b.purgeFutures().forEach(f -> stop(f)); snapshot.add(b); } } } return snapshot; } /** * Re-enable the exceptional behaviours previously disabled by * {@link #disableActiveExceptionalBehaviours()}, preserving their positions. */ public void enableExceptionalBehaviours(List<ExceptionalBehaviour> snapshot) { for (ExceptionalBehaviour b : snapshot) b.enabled = true; } public ExceptionalBehaviour getExceptionalBehaviour(Future<ExecutionResult> future) { List<ExceptionalBehaviour> excs = exceptionalBehaviours.get(); synchronized (excs) { Loading Loading
plugins/org.etsi.mts.tdl.execution.java.codegen/src/org/etsi/mts/tdl/execution/java/codegen/JUnitTestGenerator.java +6 −4 Original line number Diff line number Diff line Loading @@ -1607,8 +1607,10 @@ public class JUnitTestGenerator extends Renderer { writeNotification((ExceptionalBehaviour) bl.eContainer(), true); lineComment("Disable while exceptional behaviour is executed"); line(COMPONENT_FIELD + ".disableExceptionalBehaviour(" + exceptionalBehaviourName + ");"); lineComment("Protect the handler body: suspend all active exceptional behaviours"); lineComment("(siblings, outer defaults/interrupts and this one) for its duration."); line("List<ExceptionalBehaviour> " + exceptionalBehaviourName + "_suspended = " + COMPONENT_FIELD + ".disableActiveExceptionalBehaviours();"); newLine(); append("try"); Loading Loading @@ -1636,8 +1638,8 @@ public class JUnitTestGenerator extends Renderer { append("finally"); blockOpen(); lineComment("Enable the exceptional behaviour again"); line(COMPONENT_FIELD + ".enableExceptionalBehaviour(" + exceptionalBehaviourName + ");"); lineComment("Re-enable the suspended exceptional behaviours"); line(COMPONENT_FIELD + ".enableExceptionalBehaviours(" + exceptionalBehaviourName + "_suspended);"); writeNotification((ExceptionalBehaviour) bl.eContainer(), false); blockClose(); Loading
plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/rt/core/ReceiverHub.java +36 −12 Original line number Diff line number Diff line Loading @@ -29,9 +29,19 @@ public class ReceiverHub { // Per-thread batch state: each thread's entry is {batchSize, batchCount}. // Expectations from threads with incomplete batches are skipped by run(). private Map<Thread, int[]> batches = new HashMap<>(); // Per-thread suspended state: threads whose expectations matched are // suspended until they finish cleanup and call resume(). private Set<Thread> suspendedThreads = new HashSet<>(); /** * Shared per-round exclusivity token. Every expectable submitted as part of a * single alternative round (the alternatives, the active exceptionals and the * anyReceivers of one {@code executeAlternativesAndExceptionals} call) shares * one instance. As soon as any of them matches, {@link #run()} sets * {@link #resolved} so all remaining competitors of the same round are skipped * and never processed — realising the TDL "only one alternative Block is * executed" semantics at the instant of the match. */ public static class AlternativeRound { volatile boolean resolved; } public ReceiverHub(SystemAdapter systemAdapter, Connection connection, NamedElement testerComponent) { this.systemAdapter = systemAdapter; Loading Loading @@ -64,9 +74,13 @@ public class ReceiverHub { } } /** * Wakes the receiver loop. Retained for the generated code that calls * {@code TestControl.resumeReceiving()} after a round; round exclusivity is now * handled by {@link AlternativeRound}, so this only needs to nudge the loop. */ public void resume() { synchronized (expecting) { suspendedThreads.remove(Thread.currentThread()); expecting.notifyAll(); } } Loading @@ -74,9 +88,9 @@ public class ReceiverHub { private synchronized void run() { // Track tried expectables by identity rather than positional index, // since the expecting list is mutated concurrently by other threads. // Stale entries after resume are harmless — each hub.receive() call // creates a new Expectable instance with distinct identity, so stale // entries from previous cycles never match newly added expectables. // Stale entries are harmless — each hub.receive() call creates a new // Expectable instance with distinct identity, so stale entries from // previous cycles never match newly added expectables. Set<Expectable> tried = new HashSet<>(); while (!stopped) { Expectable currentlyExpecting = null; Loading @@ -85,7 +99,7 @@ public class ReceiverHub { while (currentlyExpecting == null) { for (Expectable e : expecting) if (!tried.contains(e) && !suspendedThreads.contains(e.ownerThread) && !(e.round != null && e.round.resolved) && !hasPendingBatch(e.ownerThread)) { currentlyExpecting = e; break; Loading Loading @@ -113,7 +127,10 @@ public class ReceiverHub { } } expecting.remove(currentlyExpecting); suspendedThreads.add(currentlyExpecting.ownerThread); // Resolve the round BEFORE notifying: the next loop iteration then // skips every remaining competitor of this round so none can match. if (currentlyExpecting.round != null) currentlyExpecting.round.resolved = true; synchronized (currentlyExpecting) { currentlyExpecting.received = data; currentlyExpecting.notifyAll(); Loading @@ -128,7 +145,8 @@ public class ReceiverHub { } catch (RuntimeException e) { currentlyExpecting.error = e; expecting.remove(currentlyExpecting); suspendedThreads.add(currentlyExpecting.ownerThread); if (currentlyExpecting.round != null) currentlyExpecting.round.resolved = true; synchronized (currentlyExpecting) { currentlyExpecting.notifyAll(); } Loading @@ -142,7 +160,11 @@ public class ReceiverHub { } public Data receive(Data expected, boolean ignoreUntil) { Expectable expectable = new Expectable(expected, ignoreUntil); return receive(expected, ignoreUntil, null); } public Data receive(Data expected, boolean ignoreUntil, AlternativeRound round) { Expectable expectable = new Expectable(expected, ignoreUntil, round); synchronized (expecting) { expecting.add(expectable); int[] batch = batches.get(Thread.currentThread()); Loading Loading @@ -193,11 +215,13 @@ public class ReceiverHub { boolean ignoreUnmatched = false; boolean anyReceiver = false; Thread ownerThread = Thread.currentThread(); AlternativeRound round; public Expectable(Data expected, boolean ignoreUnmatched) { public Expectable(Data expected, boolean ignoreUnmatched, AlternativeRound round) { this.expected = expected; this.ignoreUnmatched = ignoreUnmatched; this.anyReceiver = expected == null; this.round = round; } } }
plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/rt/core/TestControl.java +58 −3 Original line number Diff line number Diff line Loading @@ -125,7 +125,7 @@ public class TestControl { @Override public ExecutionResult call() throws Exception { try { Data data = hub.receive(null, false); Data data = hub.receive(null, false, this.round); return data != null ? new InteractionResult(data) : null; } catch (RuntimeException e) { // Propagate SA errors through the behaviour execution path: Loading Loading @@ -201,6 +201,15 @@ public class TestControl { } public abstract class ExecutionCallable implements Callable<ExecutionResult> { /** * The exclusivity round this callable was submitted under, assigned by * {@link #executeAlternativesAndExceptionals(List)} before {@link #execute()}. * Propagated to the {@link ReceiverHub.Expectable} so a match in the round * disables every competitor. Volatile: written by the submitting thread, * read by the pool thread running {@link #call()}. */ public volatile ReceiverHub.AlternativeRound round; public Future<ExecutionResult> execute() { return completionService.get().submit(this); } Loading Loading @@ -283,18 +292,30 @@ public class TestControl { // Enable batches BEFORE submitting any callables hubCounts.forEach((hub, count) -> hub.enableBatch(count)); // One exclusivity round shared by every expectable submitted here: the // alternatives, the active exceptionals and the anyReceivers. When any of // them matches, the round is resolved and the rest are disabled at once. // The round is assigned BEFORE execute() so it is visible to the pool // thread that creates the Expectable (and before batch gating releases it). ReceiverHub.AlternativeRound round = new ReceiverHub.AlternativeRound(); // Submit alternatives List<Future<ExecutionResult>> altFutures = new ArrayList<>(); for (ExecutionCallable c : alternatives) { c.round = round; altFutures.add(c.execute()); } // Submit exceptionals (priority-ordered) and per-hub anyReceivers List<Future<ExecutionResult>> excFutures = new ArrayList<>(); for (ExceptionalBehaviour exc : enabledExcs) { exc.callable.round = round; excFutures.add(exc.execute()); } receiverHubs.values().forEach(hub -> excFutures.add(hub.getAnyReceiver().execute())); receiverHubs.values().forEach(hub -> { hub.getAnyReceiver().callable.round = round; excFutures.add(hub.getAnyReceiver().execute()); }); return new BatchedFutures(altFutures, excFutures, hubCounts.keySet()); } Loading Loading @@ -402,7 +423,7 @@ public class TestControl { } @Override public ExecutionResult call() throws Exception { Data data = getReceiver().receive(expected, isTrigger); Data data = getReceiver().receive(expected, isTrigger, this.round); return data != null ? new InteractionResult(data) : null; } }; Loading Loading @@ -480,6 +501,40 @@ public class TestControl { b.enabled = true; } /** * Disable ALL currently enabled exceptional behaviours and return the snapshot. * Used to make an exceptional handler's body a protected region: while it runs, * the other exceptionals that were active when it fired (siblings, outer * defaults/interrupts, and the handler itself) are suspended so none of them * can fire during the handler. The handler's own nested exceptionals are added * after this call and therefore stay active; the implicit per-hub anyReceivers * are not in this list and remain active. Restore with * {@link #enableExceptionalBehaviours(List)}. */ public List<ExceptionalBehaviour> disableActiveExceptionalBehaviours() { List<ExceptionalBehaviour> snapshot = new ArrayList<>(); List<ExceptionalBehaviour> excs = exceptionalBehaviours.get(); synchronized (excs) { for (ExceptionalBehaviour b : excs) { if (b.enabled) { b.enabled = false; b.purgeFutures().forEach(f -> stop(f)); snapshot.add(b); } } } return snapshot; } /** * Re-enable the exceptional behaviours previously disabled by * {@link #disableActiveExceptionalBehaviours()}, preserving their positions. */ public void enableExceptionalBehaviours(List<ExceptionalBehaviour> snapshot) { for (ExceptionalBehaviour b : snapshot) b.enabled = true; } public ExceptionalBehaviour getExceptionalBehaviour(Future<ExecutionResult> future) { List<ExceptionalBehaviour> excs = exceptionalBehaviours.get(); synchronized (excs) { Loading