Commit 8973584a authored by Martti Käärik's avatar Martti Käärik
Browse files

Make alternative matching exclusive and disable exceptionals inside exceptional handler bodies.

parent 8742f14d
Loading
Loading
Loading
Loading
Loading
+6 −4
Original line number Diff line number Diff line
@@ -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");
@@ -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();
+36 −12
Original line number Diff line number Diff line
@@ -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;
@@ -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();
		}
	}
@@ -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;
@@ -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;
@@ -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();
@@ -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();
				}
@@ -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());
@@ -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;
		}
	}
}
+58 −3
Original line number Diff line number Diff line
@@ -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:
@@ -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);
		}
@@ -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());
	}
@@ -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;
			}
		};
@@ -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) {