Commit 2a530144 authored by Martti Käärik's avatar Martti Käärik
Browse files

code generator fixes #188

- exceptional behaviour precedence rules now honored
- exceptional behaviours now batched together with alternatives (to avoid any races)
- all completed notifications in finally block
- rendered exceptions (for Stop, Break etc) wrapped in TC method calls (to avoid Java compiler issues)
- render a comment hint for InlineAction missing the @Language: \"Java\" annotation
parent e616ca01
Loading
Loading
Loading
Loading
+160 −134
Original line number Diff line number Diff line
@@ -890,14 +890,12 @@ public class JUnitTestGenerator extends Renderer {

		List<FutureInfo> myFutures = new ArrayList<FutureInfo>();

		boolean writeAfter = true;
		List<String> exceptionalBehaviours = new ArrayList<String>();
		List<String[]> periodicThreads = new ArrayList<>();
		if (b instanceof AtomicBehaviour) {

			// Atomic behaviours

			// Component check
		// Component check: skip atomic behaviours that don't apply to the current
		// component. Done before opening the try wrapper so we don't leave it dangling.
		if (b instanceof AtomicBehaviour) {
			if (b instanceof ActionBehaviour) {
				ComponentInstance c = ((ActionBehaviour) b).getComponentInstance();
				if (c != null && !isCurrentComponentInstance(c))
@@ -911,6 +909,109 @@ public class JUnitTestGenerator extends Renderer {
				if (!isCurrentComponentInstance(v.getComponentInstance()))
					return;
			}
		}

		// Set up periodic threads and exceptional behaviours BEFORE the try block so the
		// variables they declare (Thread, Throwable[], ExceptionalBehaviour) are in scope
		// in the finally block below.
		if (b instanceof CombinedBehaviour) {

			for (PeriodicBehaviour pb : ((CombinedBehaviour) b).getPeriodic()) {
				Optional<LocalExpression> periodExp = pb.getPeriod().stream()
						.filter(le -> isCurrentComponentInstance(le.getComponentInstance())).findFirst();
				if (periodExp.isEmpty())
					continue;

				newLine();
				lineComment(pb.eClass().getName());

				DataUse periodValue = periodExp.get().getExpression();
				initializeDataUse(periodValue, dataUseVariables);

				String baseName = getElementName(pb);
				String threadName = "periodic_" + baseName;
				String errorName = "periodicError_" + baseName;
				String periodVar = "period_" + baseName;

				// Store period value before thread creation (must be effectively final)
				append("long " + periodVar + " = ");
				write(periodValue, dataUseVariables);
				line(";");

				line("Throwable[] " + errorName + " = new Throwable[1];");
				append("Thread " + threadName + " = new Thread(() -> ");
				blockOpen();
				append("try ");
				blockOpen();

				// Fixed-rate: next execution time tracks start-to-start interval
				line("long nextExecution = System.currentTimeMillis() + " + periodVar + ";");
				append("while (!Thread.interrupted()) ");
				blockOpen();
				line("long sleepTime = nextExecution - System.currentTimeMillis();");
				line("if (sleepTime > 0) Thread.sleep(sleepTime);");

				write(pb.getBlock(), null, null, thrownExceptions);

				line("nextExecution += " + periodVar + ";");
				blockClose();

				blockClose();
				append("catch (" + INTERRUPTED_EXCEPTION + " e) {} ");
				append("catch (Throwable e) ");
				blockOpen();
				append("synchronized (" + errorName + ") ");
				blockOpen();
				line("if (" + errorName + "[0] == null) " + errorName + "[0] = e;");
				blockClose();
				blockClose();

				blockClose();
				line(");");
				line(threadName + ".start();");

				periodicThreads.add(new String[]{threadName, errorName});
			}

			// Iterate exceptionals in REVERSE declaration order: TestControl prioritises
			// later-added entries, but within a single CombinedBehaviour the FIRST-declared
			// exceptional has the highest priority. Adding them in reverse maps declaration
			// order to priority correctly.
			List<ExceptionalBehaviour> exceptionalsList = ((CombinedBehaviour) b).getExceptional();
			for (int ebIdx = exceptionalsList.size() - 1; ebIdx >= 0; ebIdx--) {
				ExceptionalBehaviour eb = exceptionalsList.get(ebIdx);

				newLine();
				lineComment(eb.eClass().getName());

				ComponentInstance gc = eb.getGuardedComponent();
				if (gc != null && !isCurrentComponentInstance(gc))
					continue;

				String exceptionalBehaviourName = getElementName(eb);
				append("ExceptionalBehaviour " + exceptionalBehaviourName + " = new ExceptionalBehaviour(");
				append(Boolean.toString(eb instanceof InterruptBehaviour));
				line(");");

				// TODO what to do with those?
				Set<String> innerExceptions = new HashSet<String>();
				write(eb.getBlock(), exceptionalBehaviourName, null, innerExceptions);

				line(COMPONENT_FIELD + ".addExceptionalBehaviour(" + exceptionalBehaviourName + ");");
				newLine();
				exceptionalBehaviours.add(exceptionalBehaviourName);
			}
		}

		// Wrap body emission in try { ... } finally { cleanup } so post-processing
		// (periodic thread joins, exceptional removal, completed-notification, objective)
		// always runs — even when a child behaviour throws (terminate / break / assertion).
		append("try ");
		blockOpen();

		if (b instanceof AtomicBehaviour) {

			// Atomic behaviours

			// Timing
			TimeLabel timeLabel = ((AtomicBehaviour) b).getTimeLabel();
@@ -961,6 +1062,8 @@ public class JUnitTestGenerator extends Renderer {
						a -> a.getKey().getName().equals(LANGUAGE_KEY) && a.getValue().equals(JAVA_LANGUAGE_VALUE))) {
					append(((InlineAction) b).getBody());
					newLine();
				} else {
					lineComment("Add annotation @Language: \"Java\" to your inline action");
				}

			}
@@ -993,9 +1096,6 @@ public class JUnitTestGenerator extends Renderer {
					append("VerdictImpl.fail");
				line(");");

				writeNotification(b, false);
				writeObjective(b);
				writeAfter = false;
				line("throw new " + ASSERTION_EXCEPTION + "(\"" + getMessage(b) + "\");");

				blockClose();
@@ -1014,20 +1114,16 @@ public class JUnitTestGenerator extends Renderer {
					throw new RuntimeException("Break not contained in a Block " + getQName(b));
				Block bl = (Block) ((Break) b).container();

				writeNotification(b, false);
				writeObjective(b);
				writeAfter = false;

				line("throw new " + BREAK_EXCEPTION + "(\"" + getElementName(bl) + "\");");
				// Indirect throw via helper so the JLS reachability check accepts any
				// generated code that follows (e.g. cleanup or trailing siblings).
				line(COMPONENT_FIELD + ".breakBlock(\"" + getElementName(bl) + "\");");
				thrownExceptions.add(BREAK_EXCEPTION);

			} else if (b instanceof Stop) {

				writeNotification(b, false);
				writeObjective(b);
				writeAfter = false;

				line("throw new " + STOP_EXCEPTION + "Impl" + "(\"Stop " + getQName(b) + "\");");
				// Indirect throw via helper so the JLS reachability check accepts any
				// generated code that follows (e.g. cleanup or trailing siblings).
				line(COMPONENT_FIELD + ".terminate(\"Stop " + getQName(b) + "\");");
				thrownExceptions.add(STOP_EXCEPTION);

			}
@@ -1115,89 +1211,11 @@ public class JUnitTestGenerator extends Renderer {
		}

		// Combined behaviours
		// (Periodic threads and exceptional behaviours have already been set up
		// before the try block above so the variables are visible in finally.)

		else if (b instanceof CombinedBehaviour) {

			for (PeriodicBehaviour pb : ((CombinedBehaviour) b).getPeriodic()) {
				Optional<LocalExpression> periodExp = pb.getPeriod().stream()
						.filter(le -> isCurrentComponentInstance(le.getComponentInstance())).findFirst();
				if (periodExp.isEmpty())
					continue;

				newLine();
				lineComment(pb.eClass().getName());

				DataUse periodValue = periodExp.get().getExpression();
				initializeDataUse(periodValue, dataUseVariables);

				String baseName = getElementName(pb);
				String threadName = "periodic_" + baseName;
				String errorName = "periodicError_" + baseName;
				String periodVar = "period_" + baseName;

				// Store period value before thread creation (must be effectively final)
				append("long " + periodVar + " = ");
				write(periodValue, dataUseVariables);
				line(";");

				line("Throwable[] " + errorName + " = new Throwable[1];");
				append("Thread " + threadName + " = new Thread(() -> ");
				blockOpen();
				append("try ");
				blockOpen();

				// Fixed-rate: next execution time tracks start-to-start interval
				line("long nextExecution = System.currentTimeMillis() + " + periodVar + ";");
				append("while (!Thread.interrupted()) ");
				blockOpen();
				line("long sleepTime = nextExecution - System.currentTimeMillis();");
				line("if (sleepTime > 0) Thread.sleep(sleepTime);");

				write(pb.getBlock(), null, null, thrownExceptions);

				line("nextExecution += " + periodVar + ";");
				blockClose();

				blockClose();
				append("catch (" + INTERRUPTED_EXCEPTION + " e) {} ");
				append("catch (Throwable e) ");
				blockOpen();
				append("synchronized (" + errorName + ") ");
				blockOpen();
				line("if (" + errorName + "[0] == null) " + errorName + "[0] = e;");
				blockClose();
				blockClose();

				blockClose();
				line(");");
				line(threadName + ".start();");

				periodicThreads.add(new String[]{threadName, errorName});
			}

			for (ExceptionalBehaviour eb : ((CombinedBehaviour) b).getExceptional()) {

				newLine();
				lineComment(eb.eClass().getName());

				ComponentInstance gc = eb.getGuardedComponent();
				if (gc != null && !isCurrentComponentInstance(gc))
					continue;

				String exceptionalBehaviourName = getElementName(eb);
				append("ExceptionalBehaviour " + exceptionalBehaviourName + " = new ExceptionalBehaviour(");
				append(Boolean.toString(eb instanceof InterruptBehaviour));
				line(");");

				// TODO what to do with those?
				Set<String> innerExceptions = new HashSet<String>();
				write(eb.getBlock(), exceptionalBehaviourName, null, innerExceptions);

				line(COMPONENT_FIELD + ".addExceptionalBehaviour(" + exceptionalBehaviourName + ");");
				newLine();
				exceptionalBehaviours.add(exceptionalBehaviourName);
			}

			// Single block
			if (b instanceof CompoundBehaviour) {
				write(((CompoundBehaviour) b).getBlock(), null, null, thrownExceptions);
@@ -1368,6 +1386,7 @@ public class JUnitTestGenerator extends Renderer {
					newLine();
				}

				String batchName = "batch_" + getElementName(b);
				String futuresName = "futures_" + getElementName(b);
				String excName = "exc_" + getElementName(b);
				String winnerName = "winner_" + getElementName(b);
@@ -1377,11 +1396,14 @@ public class JUnitTestGenerator extends Renderer {
				append("while (true)");
				blockOpen();

				// Submit all callables (with batch mode on ReceiverHubs)
				line("List<Future<ExecutionResult>> " + futuresName + " = " + COMPONENT_FIELD
						+ ".executeAlternatives(" + callablesListName + ");");
				line("List<Future<ExecutionResult>> " + excName + " = " + COMPONENT_FIELD
						+ ".executeExceptionals();");
				// Submit alternatives + exceptionals + anyReceivers as a single batch on
				// every involved ReceiverHub: the hub does not start matching until every
				// expectable has been added, so an exceptional cannot lose its message to
				// an alternative simply because of submission ordering.
				line("BatchedFutures " + batchName + " = " + COMPONENT_FIELD
						+ ".executeAlternativesAndExceptionals(" + callablesListName + ");");
				line("List<Future<ExecutionResult>> " + futuresName + " = " + batchName + ".alternatives;");
				line("List<Future<ExecutionResult>> " + excName + " = " + batchName + ".exceptionals;");
				newLine();

				// Wait for first completion
@@ -1417,7 +1439,7 @@ public class JUnitTestGenerator extends Renderer {
						// Single tester-input: value assignments + timeout handling
						writeResultHandling(f.b, dataUseVariables);
						if (f.kind.equals("TimeoutResult")) {
							line("throw new " + STOP_EXCEPTION + "Impl" + "(\"Timeout\");");
							line(COMPONENT_FIELD + ".terminate(\"Timeout\");");
							thrownExceptions.add(STOP_EXCEPTION);
						}
					}
@@ -1448,7 +1470,7 @@ public class JUnitTestGenerator extends Renderer {
				blockOpen();

				// Cleanup
				line(COMPONENT_FIELD + ".cleanupAlternatives(" + callablesListName + ");");
				line(COMPONENT_FIELD + ".cleanupBatch(" + batchName + ");");
				line(futuresName + ".forEach(f -> " + COMPONENT_FIELD + ".stop(f));");
				line(excName + ".forEach(f -> " + COMPONENT_FIELD + ".stop(f));");
				line(COMPONENT_FIELD + ".resumeReceiving();");
@@ -1466,7 +1488,10 @@ public class JUnitTestGenerator extends Renderer {
			futures.addAll(myFutures);
		}
		
		if (writeAfter) {
		blockClose();
		append("finally ");
		blockOpen();

		// Stop periodic threads and propagate errors
		for (String[] pt : periodicThreads) {
			String threadName = pt[0];
@@ -1494,7 +1519,8 @@ public class JUnitTestGenerator extends Renderer {
		exceptionalBehaviours.forEach(eb -> {
			line(COMPONENT_FIELD + ".removeExceptionalBehaviour(" + eb + ");");
		});
		}

		blockClose();

	}

@@ -1577,7 +1603,7 @@ public class JUnitTestGenerator extends Renderer {
						blockOpen();

						lineComment("Disable while exceptional behaviour is executed");
						line(COMPONENT_FIELD + ".removeExceptionalBehaviour(" + exceptionalBehaviourName + ");");
						line(COMPONENT_FIELD + ".disableExceptionalBehaviour(" + exceptionalBehaviourName + ");");
						newLine();

						append("try");
@@ -1606,7 +1632,7 @@ public class JUnitTestGenerator extends Renderer {
			blockOpen();

			lineComment("Enable the exceptional behaviour again");
			line(COMPONENT_FIELD + ".addExceptionalBehaviour(" + exceptionalBehaviourName + ");");
			line(COMPONENT_FIELD + ".enableExceptionalBehaviour(" + exceptionalBehaviourName + ");");
			blockClose();
			blockCloseMethod();

@@ -1668,7 +1694,7 @@ public class JUnitTestGenerator extends Renderer {
			String futureName = "timeout_" + getElementName(b);

			String timerName = getTimerName(((TimerOperation) b).getTimer());
			append("ExecutionCallable timeout_" + getElementName(b) + " = " + COMPONENT_FIELD + ".timeout(" + timerName + ")");
			append("ExecutionCallable timeout_" + getElementName(b) + " = " + COMPONENT_FIELD + ".timeout(" + COMPONENT_FIELD + "." + timerName + ")");
			line(";");

			return new FutureInfo(futureName, "TimeoutResult", b);
+8 −0
Original line number Diff line number Diff line
@@ -24,6 +24,14 @@ public class ExceptionalBehaviour {
	 */
	public boolean isInterrupt;

	/**
	 * Whether the behaviour is currently active. Disabled exceptional behaviours
	 * remain in the test control's list (so their position / priority is preserved)
	 * but are skipped by the matching logic. Used to suppress re-entry while the
	 * exceptional's own handler is running.
	 */
	public boolean enabled = true;
	
	/**
	 * <p>The futures for the triggering behaviour (callable). Several futures may exist
	 * simultaneously due to alternatives. Several threads may have their own set of
+130 −29
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.rt.core;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
@@ -23,6 +24,7 @@ import org.etsi.mts.tdl.execution.java.tri.GateReference;
import org.etsi.mts.tdl.execution.java.tri.NamedElement;
import org.etsi.mts.tdl.execution.java.tri.PredefinedFunctions;
import org.etsi.mts.tdl.execution.java.tri.Reporter;
import org.etsi.mts.tdl.execution.java.tri.StopException;
import org.etsi.mts.tdl.execution.java.tri.SystemAdapter;
import org.etsi.mts.tdl.execution.java.tri.Validator;

@@ -110,10 +112,11 @@ public class TestControl {
			receiverHubs.put(c, hub);

			// TODO: anyReceiver is currently only active during tester-input processing
			// (submitted via executeExceptionals). Consider making it always active to
			// catch unexpected messages between tester-input steps. Check if TDL spec
			// mandates this. Would require a dedicated thread that permanently blocks
			// on hub.receive(null, false) and propagates failures to the main test thread.
			// (submitted as part of executeAlternativesAndExceptionals). Consider making
			// it always active to catch unexpected messages between tester-input steps.
			// Check if TDL spec mandates this. Would require a dedicated thread that
			// permanently blocks on hub.receive(null, false) and propagates failures to
			// the main test thread.
			ExceptionalBehaviour anyReceiver = new ExceptionalBehaviour(false);
			anyReceiver.behaviour = () -> {
				throw new StopExceptionImpl("Unexpected message received");
@@ -215,45 +218,118 @@ public class TestControl {
		public abstract ReceiverHub getReceiver();
	}

	public List<Future<ExecutionResult>> executeAlternatives(List<ExecutionCallable> callables) {
		// Count InteractionCallables per hub and enable batch mode
	/**
	 * Result of {@link #executeAlternativesAndExceptionals(List)}: separate future
	 * lists for the alternative and exceptional callables, both submitted under a
	 * single batched receive on every involved {@link ReceiverHub}. The set of
	 * touched hubs is also captured so {@link #cleanupBatch(BatchedFutures)} can
	 * disable batch mode on exactly those hubs and nothing else.
	 */
	public static class BatchedFutures {
		public final List<Future<ExecutionResult>> alternatives;
		public final List<Future<ExecutionResult>> exceptionals;
		public final Collection<ReceiverHub> batchedHubs;

		public BatchedFutures(List<Future<ExecutionResult>> alternatives,
				List<Future<ExecutionResult>> exceptionals,
				Collection<ReceiverHub> batchedHubs) {
			this.alternatives = alternatives;
			this.exceptionals = exceptionals;
			this.batchedHubs = batchedHubs;
		}
	}

	/**
	 * Submit alternative callables together with all currently enabled exceptional
	 * behaviour callables (and per-hub anyReceivers) as a single batch on every
	 * affected {@link ReceiverHub}, so the hub does not start matching until every
	 * expectable from this round has been added. Without this, the hub could match
	 * an alternative against a message that an exceptional was meant to consume.
	 *
	 * <p>Exceptional callables are submitted in reverse list order so that the
	 * later-added entries (inner CombinedBehaviours, and within a CombinedBehaviour
	 * the first-declared exceptional) end up at the front of the hub's expecting
	 * list and therefore win matches first.
	 */
	public BatchedFutures executeAlternativesAndExceptionals(List<ExecutionCallable> alternatives) {
		// Snapshot enabled exceptionals in priority order (reverse list order)
		List<ExceptionalBehaviour> enabledExcs = new ArrayList<>();
		List<ExceptionalBehaviour> excs = exceptionalBehaviours.get();
		synchronized (excs) {
			for (int i = excs.size() - 1; i >= 0; i--) {
				ExceptionalBehaviour exc = excs.get(i);
				if (exc.enabled)
					enabledExcs.add(exc);
			}
		}

		// Count expectables per hub: alternatives + exceptional callables + anyReceivers
		Map<ReceiverHub, Integer> hubCounts = new HashMap<>();
		for (ExecutionCallable c : callables) {
		for (ExecutionCallable c : alternatives) {
			if (c instanceof InteractionCallable) {
				ReceiverHub hub = ((InteractionCallable) c).getReceiver();
				hubCounts.merge(hub, 1, Integer::sum);
				hubCounts.merge(((InteractionCallable) c).getReceiver(), 1, Integer::sum);
			}
		}
		for (ExceptionalBehaviour exc : enabledExcs) {
			if (exc.callable instanceof InteractionCallable) {
				hubCounts.merge(((InteractionCallable) exc.callable).getReceiver(), 1, Integer::sum);
			}
		}
		// One anyReceiver per hub
		for (ReceiverHub hub : receiverHubs.values()) {
			hubCounts.merge(hub, 1, Integer::sum);
		}

		// Enable batches BEFORE submitting any callables
		hubCounts.forEach((hub, count) -> hub.enableBatch(count));

		// Submit all callables
		List<Future<ExecutionResult>> futures = new ArrayList<>();
		for (ExecutionCallable c : callables) {
			futures.add(c.execute());
		}
		return futures;
		// Submit alternatives
		List<Future<ExecutionResult>> altFutures = new ArrayList<>();
		for (ExecutionCallable c : alternatives) {
			altFutures.add(c.execute());
		}

	public void cleanupAlternatives(List<ExecutionCallable> callables) {
		for (ExecutionCallable c : callables) {
			if (c instanceof InteractionCallable) {
				((InteractionCallable) c).getReceiver().disableBatch();
		// Submit exceptionals (priority-ordered) and per-hub anyReceivers
		List<Future<ExecutionResult>> excFutures = new ArrayList<>();
		for (ExceptionalBehaviour exc : enabledExcs) {
			excFutures.add(exc.execute());
		}
		receiverHubs.values().forEach(hub -> excFutures.add(hub.getAnyReceiver().execute()));

		return new BatchedFutures(altFutures, excFutures, hubCounts.keySet());
	}

	/**
	 * Disable batch mode on the hubs that were enabled by the corresponding
	 * {@link #executeAlternativesAndExceptionals(List)} call. Avoids unnecessary
	 * {@code notifyAll} on hubs that were never batched.
	 */
	public void cleanupBatch(BatchedFutures batch) {
		batch.batchedHubs.forEach(ReceiverHub::disableBatch);
	}

	public void resumeReceiving() {
		receiverHubs.values().forEach(hub -> hub.resume());
	}

	public List<Future<ExecutionResult>> executeExceptionals() {
		List<Future<ExecutionResult>> futures = new ArrayList<>();
		List<ExceptionalBehaviour> excs = exceptionalBehaviours.get();
		synchronized (excs) {
			excs.forEach(exc -> futures.add(exc.execute()));
	/**
	 * Throws a {@link StopException} to terminate the test execution. Wrapping
	 * the throw in a method call lets the Java compiler treat the call site as
	 * potentially completing normally — generated code can therefore have
	 * statements after a TDL {@code terminate} (or after timeout handling)
	 * without tripping the unreachable-code check.
	 */
	public void terminate(String message) throws StopException {
		throw new StopExceptionImpl(message);
	}
		receiverHubs.values().forEach(hub -> futures.add(hub.getAnyReceiver().execute()));
		return futures;

	/**
	 * Throws a {@link BreakException} for the named block. Same rationale as
	 * {@link #terminate(String)}: the call form keeps the compiler happy with
	 * subsequent generated statements.
	 */
	public void breakBlock(String blockName) throws BreakException {
		throw new BreakException(blockName);
	}

	public ExecutionCallable timeConstraint(Constraint constraint) {
@@ -365,8 +441,14 @@ public class TestControl {
		return c;
	}

	/**
	 * Inner exceptional behaviours and ones declared earlier within the same
	 * CombinedBehaviour must have precedence. The matching logic favours later
	 * entries in the list, so callers add outer-first / inner-last and reverse the
	 * declaration order within a single CombinedBehaviour.
	 */
	public void addExceptionalBehaviour(ExceptionalBehaviour b) {
		// TODO Inner exceptional behaviours have precedence

		List<ExceptionalBehaviour> excs = exceptionalBehaviours.get();
		synchronized (excs) {
			if (!excs.contains(b))
@@ -379,6 +461,25 @@ public class TestControl {
		b.purgeFutures().forEach(f -> stop(f));
	}

	/**
	 * Temporarily deactivate an exceptional behaviour without removing it from the
	 * list. Used while the exceptional's own handler is running, so it does not
	 * re-fire on itself, while preserving its priority position for when it is
	 * re-enabled.
	 */
	public void disableExceptionalBehaviour(ExceptionalBehaviour b) {
		b.enabled = false;
		b.purgeFutures().forEach(f -> stop(f));
	}

	/**
	 * Re-activate a previously disabled exceptional behaviour. Position in the
	 * exceptional behaviour list (and therefore priority) is preserved.
	 */
	public void enableExceptionalBehaviour(ExceptionalBehaviour b) {
		b.enabled = true;
	}

	public ExceptionalBehaviour getExceptionalBehaviour(Future<ExecutionResult> future) {
		List<ExceptionalBehaviour> excs = exceptionalBehaviours.get();
		synchronized (excs) {