Commit 4f22d8e7 authored by Martti Käärik's avatar Martti Käärik
Browse files

Log behaviour properties via Reporter #135 #173

parent 09809c24
Loading
Loading
Loading
Loading
+120 −24
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -142,6 +143,24 @@ public class JUnitTestGenerator extends Renderer {

	private boolean logEvents = true;

	/**
	 * Outcome properties (as Java expressions) for each behaviour to be used with behaviourCompleted()
	 */
	private Map<Element, Map<String, String>> outcomeProperties = new Hashtable<Element, Map<String, String>>();

	private void addOutcomeProperty(Element b, String key, String valueExpression) {
		outcomeProperties.computeIfAbsent(b, e -> new LinkedHashMap<String, String>()).put(key, valueExpression);
	}

	private String getOutcomePropertiesExpression(Element b) {
		List<String> keyValues = new ArrayList<String>();
		outcomeProperties.getOrDefault(b, Map.of()).forEach((key, value) -> {
			keyValues.add(PROPERTY_KEYS + "." + key);
			keyValues.add(value);
		});
		return PROPERTIES_CLASS + ".of(" + String.join(", ", keyValues) + ")";
	}

	protected Map<MappableDataElement, Set<DataElementMapping>> elementMappings = new Hashtable<MappableDataElement, Set<DataElementMapping>>();

	protected Map<Element, String> classNames = new Hashtable<Element, String>();
@@ -156,6 +175,13 @@ public class JUnitTestGenerator extends Renderer {
	 */
	private static final String TEMPLATE_VARIABLE_SUFFIX = "_t";

	/**
	 * Runtime class referenced by the generated reporter calls: the property key
	 * constants and the builder of property maps ({@code of(...)}).
	 */
	private static final String PROPERTY_KEYS = CORE_PACKAGE + ".BehaviourProperties",
			PROPERTIES_CLASS = PROPERTY_KEYS;

	public JUnitTestGenerator(Package model, Settings settings) {
		super(settings.outputFile, settings.outPackage);
		this.model = model;
@@ -568,6 +594,8 @@ public class JUnitTestGenerator extends Renderer {

		imports.add("java.util.concurrent.Future");
		imports.add("java.util.List");
		imports.add("java.util.Map");
		imports.add("java.util.Hashtable");

		imports.add(TRI_PACKAGE + ".*");
		imports.add(CORE_PACKAGE + ".*");
@@ -1043,6 +1071,10 @@ public class JUnitTestGenerator extends Renderer {
			}
		}

		// Outcome variables reported by the completed-notification in the finally
		// block below must be declared before the try block
		writeOutcomeVariableDeclarations(b);

		// 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).
@@ -1119,14 +1151,33 @@ public class JUnitTestGenerator extends Renderer {
			}
			// Verdicts
			else if (b instanceof Assertion) {
				initializeDataUse(((Assertion) b).getCondition(), dataUseVariables);
				DataUse condition = ((Assertion) b).getCondition();
				initializeDataUse(condition, dataUseVariables);
				DataUse verdict = ((Assertion) b).getOtherwise();
				if (verdict != null)
					initializeDataUse(verdict, dataUseVariables);

				append("if (!(");
				write(((Assertion) b).getCondition(), dataUseVariables);
				append("))");
				// Evaluate once; the result and the operands are reported as the
				// outcome of the assertion by the completion notification (in the
				// finally block, so also when the assertion error is thrown below)
				String resultVar = getAssertionResultVariable(b);
				append(resultVar + " = (");
				write(condition, dataUseVariables);
				line(");");
				List<DataUse> operands = getAssertionOperands(condition);
				if (!operands.isEmpty()) {
					// Capture the evaluated operands for the outcome report
					append(getAssertionOperandsVariable(b) + " = new Object[] { ");
					for (int i = 0; i < operands.size(); i++) {
						if (i > 0)
							append(", ");
						write(operands.get(i), dataUseVariables);
					}
					line(" };");
				}
				addAssertionOutcomeProperties(b, condition);

				line("if (!" + resultVar + ")");
				blockOpen();

				append(VALIDATOR_FIELD + ".setVerdict(");
@@ -1136,7 +1187,7 @@ public class JUnitTestGenerator extends Renderer {
					append("VerdictImpl.fail");
				line(");");

				writeAssertionFailure(b, ((Assertion) b).getCondition(), dataUseVariables);
				writeAssertionFailure(b, condition, dataUseVariables);

				blockClose();

@@ -1333,6 +1384,7 @@ public class JUnitTestGenerator extends Renderer {
					}

					// Create callable (without submitting)
					writeOutcomeVariableDeclarations(triggerBehaviour);
					FutureInfo callable = writeTesterInput(triggerBehaviour, dataUseVariables);
					callable.altBlock = bl;
					callable.altTriggerIndex = triggerIndex;
@@ -1585,6 +1637,18 @@ public class JUnitTestGenerator extends Renderer {
			}
		}
	}
	/**
	 * Declares the variables holding the outcome of a behaviour and registers them as
	 * outcome properties. Emitted before the try block of the behaviour, or for an
	 * alternative trigger before its callable is created.
	 */
	private void writeOutcomeVariableDeclarations(Behaviour b) {
		if (b instanceof Assertion) {
			line("boolean " + getAssertionResultVariable(b) + " = false;");
			if (!getAssertionOperands(((Assertion) b).getCondition()).isEmpty())
				line("Object[] " + getAssertionOperandsVariable(b) + " = null;");
		}
	}

	private FutureInfo writeTesterInput(Behaviour b, Map<DataUse, String> dataUseVariables) {
		if (b instanceof TimeOut || b instanceof Quiescence || b instanceof Message || b instanceof ProcedureCall) {
@@ -2803,37 +2867,37 @@ public class JUnitTestGenerator extends Renderer {

	private void writeNotification(Element b, boolean started) {
		if (logEvents) {
			String behaviour = "";
			if (settings.logBehaviourTraces && b instanceof AtomicBehaviour && b.eResource() instanceof XtextResource) {
				String path = b.eResource().getURI().toPlatformString(false);
				ICompositeNode node = NodeModelUtils.getNode(b);
				List<String> lines = List.of(node.getText().split("\\n")).stream()
					.map(l -> l.trim().replaceAll("\\\"", "\\\\\""))
					.filter(l -> !l.startsWith("//") && !l.isBlank())
					.toList();
				String text = String.join("", lines);
				//TODO: alternative representation -> expose as setting?
				//behaviour = "\\n  " + path + ":"+node.getStartLine() + "\\n    " +text;
				//TODO: make clickable
				behaviour = " | " + path + ":"+node.getStartLine() + " | " +text;
			}
			append(REPORTER_FIELD + ".");
			append(started ? "behaviourStarted" : "behaviourCompleted");
			append("(");
			append(COMPONENT_FIELD + ".getTesterComponent().getName(), ");
			if (started)
				append("\"" + b.eClass().getName() + "\", ");
			append("\"" + getQName(b) + behaviour + "\""); //TODO: move to better position?
			append("\"" + getQName(b) + "\", ");
			if (started)
				writeStartProperties(b);
			else
				append(getOutcomePropertiesExpression(b));
			line(");");
			// TODO properties
		}
	}

	/**
	 * Provides a descriptive name of the behaviour for informative purposes.
	 * 
	 * @return A descriptive name of the behaviour.
	 * Writes the initial properties of a behaviour as specified.
	 */
	private void writeStartProperties(Element b) {
		List<String> keyValues = new ArrayList<String>();
		if (settings.logBehaviourTraces && b instanceof AtomicBehaviour && b.eResource() instanceof XtextResource) {
			ICompositeNode node = NodeModelUtils.getNode(b);
			if (node != null) {
				String path = b.eResource().getURI().toPlatformString(false);
				keyValues.add(PROPERTY_KEYS + ".SOURCE");
				keyValues.add("\"" + escape(path + ":" + node.getStartLine()) + "\"");
			}
		}
		append(PROPERTIES_CLASS + ".of(" + String.join(", ", keyValues) + ")");
	}

	private String getQName(Element b) {
		// XXX
		String name = b.getName();
@@ -2847,6 +2911,38 @@ public class JUnitTestGenerator extends Renderer {
		return b.eClass().getName() + ": " + b.getName();
	}

	private String getAssertionResultVariable(Element assertion) {
		return getElementName(assertion) + "_result";
	}

	private String getAssertionOperandsVariable(Element assertion) {
		return getElementName(assertion) + "_operands";
	}

	/**
	 * The operands of the top-level operator of an assertion condition, or an empty
	 * list if the condition is not an operator application with several operands.
	 */
	private List<DataUse> getAssertionOperands(DataUse condition) {
		DataUse top = condition;
		if (top instanceof CastDataUse)
			top = ((CastDataUse) top).getDataUse();
		List<DataUse> operands = (top instanceof PredefinedFunctionCall) ? getDataUseArgumentValues(top)
				: Collections.emptyList();
		return operands.size() > 1 ? operands : Collections.emptyList();
	}

	private void addAssertionOutcomeProperties(Element assertion, DataUse condition) {
		addOutcomeProperty(assertion, "RESULT", getAssertionResultVariable(assertion));
		if (!getAssertionOperands(condition).isEmpty()) {
			DataUse top = condition instanceof CastDataUse ? ((CastDataUse) condition).getDataUse() : condition;
			String fn = ((PredefinedFunctionCall) top).getFunction().getName();
			addOutcomeProperty(assertion, "OPERATOR", "\"" + escape(fn) + "\"");
			addOutcomeProperty(assertion, "OPERANDS", getAssertionOperandsVariable(assertion));
		} else {
			addOutcomeProperty(assertion, "VALUE", getAssertionResultVariable(assertion));
		}
	}

	private void writeAssertionFailure(Behaviour b, DataUse condition, Map<DataUse, String> dataUseVariables) {
		DataUse top = condition;
+93 −6
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.adapters;

import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import org.etsi.mts.tdl.execution.java.tri.Data;
import org.etsi.mts.tdl.execution.java.tri.Reporter;
import org.etsi.mts.tdl.execution.java.tri.Validator;
@@ -71,15 +76,97 @@ public class DefaultAdapter implements Validator, Reporter {
	}

	@Override
	public void behaviourStarted(String tester, String kind, String id, Object... properties) {
		// TODO Auto-generated method stub
		System.out.println("[" + tester + "] Started: " + kind + " | id = " + id);
	public void behaviourStarted(String tester, String kind, String id, Map<String, Object> properties) {
		System.out.println("[" + tester + "] Started: " + kind + " | id = " + id + describe(properties));
	}

	@Override
	public void behaviourCompleted(String tester, String id) {
		// TODO Auto-generated method stub
		System.out.println("[" + tester + "] Completed: id = " + id);
	public void behaviourCompleted(String tester, String id, Map<String, Object> properties) {
		System.out.println("[" + tester + "] Completed: id = " + id + describe(properties));
	}

	/**
	 * Renders behaviour properties as <code> | {key: value, ...}</code>. Keys
	 * sharing a camelCase prefix are grouped under that prefix. {@link Data} values
	 * are rendered with their template, arrays and collections element-wise.
	 */
	protected String describe(Map<String, Object> properties) {
		if (properties == null || properties.isEmpty())
			return "";
		return " | " + describe(group(properties), true);
	}

	/**
	 * Groups keys under the longest camelCase prefix they share with at least one
	 * other key; values of grouped keys are nested maps keyed by the remainder decapitalized.
	 */
	private Map<String, Object> group(Map<String, Object> properties) {
		Map<String, Object> grouped = new LinkedHashMap<>();
		for (Map.Entry<String, Object> e : properties.entrySet()) {
			String key = e.getKey();
			String prefix = null;
			for (int i = key.length() - 1; i > 0; i--) {
				if (!Character.isUpperCase(key.charAt(i)))
					continue;
				String candidate = key.substring(0, i);
				boolean shared = properties.keySet().stream().anyMatch(
						k -> !k.equals(key) 
							&& k.startsWith(candidate)
							&& k.length() > candidate.length() 
							&& Character.isUpperCase(k.charAt(candidate.length())));
				if (shared) {
					prefix = candidate;
					break;
				}
			}
			if (prefix == null) {
				grouped.put(key, e.getValue());
			} else {
				String rest = key.substring(prefix.length());
				rest = Character.toLowerCase(rest.charAt(0)) + rest.substring(1);
				Object existing = grouped.get(prefix);
				@SuppressWarnings("unchecked")
				Map<String, Object> sub = existing instanceof Map
						? (Map<String, Object>) existing
						: new LinkedHashMap<String, Object>();
				sub.put(rest, e.getValue());
				grouped.put(prefix, sub);
			}
		}
		return grouped;
	}

	private String describe(Map<String, Object> map, boolean nested) {
		StringBuilder out = new StringBuilder("{");
		boolean first = true;
		for (Map.Entry<String, Object> e : map.entrySet()) {
			out.append(first ? "" : ", ").append(e.getKey()).append(": ").append(describe(e.getValue()));
			first = false;
		}
		return out.append("}").toString();
	}

	@SuppressWarnings("unchecked")
	protected String describe(Object value) {
		if (value instanceof Data)
			return ValueRenderer.isValue((Data<?, ?>) value) ? ValueRenderer.render((Data<?, ?>) value)
					: PojoRenderer.render((Data<?, ?>) value);
		if (value instanceof Map)
			return describe((Map<String, Object>) value, true);
		if (value instanceof Object[])
			return describe(Arrays.asList((Object[]) value));
		if (value instanceof Collection) {
			StringBuilder out = new StringBuilder("[");
			boolean first = true;
			for (Object o : (Collection<?>) value) {
				out.append(first ? "" : ", ").append(describe(o));
				first = false;
			}
			return out.append("]").toString();
		}
		if (value instanceof CharSequence)
			return "\"" + value + "\"";
		return String.valueOf(value);
	}

	@Override
+81 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.adapters;

import java.util.List;

import org.etsi.mts.tdl.execution.java.rt.core.TemplateImpl;
import org.etsi.mts.tdl.execution.java.tri.Data;
import org.etsi.mts.tdl.execution.java.tri.Template;
import org.etsi.mts.tdl.execution.java.tri.Type;
import org.etsi.mts.tdl.execution.java.tri.Value;

/**
 * Renders unmapped data (values and templates) for logging: special values in
 * TDL notation, JSON-like otherwise, named instances by qualified name.
 */
public final class ValueRenderer {

	private ValueRenderer() {
	}

	/**
	 * Whether data is unmapped.
	 */
	public static boolean isValue(Data<?, ?> data) {
		return data != null && data.getValue() instanceof Value;
	}

	@SuppressWarnings("unchecked")
	public static String render(Data<?, ?> data) {
		if (data == null)
			return "null";
		StringBuilder out = new StringBuilder();
		render((Data<Type, Value>) data, data.getTemplate(), out);
		return out.toString();
	}

	private static void render(Data<Type, Value> data, Template template, StringBuilder out) {
		if (template == null)
			template = TemplateImpl.NONE;
		if (template.getSpecialValue() != null) {
			out.append(PojoRenderer.symbol(template.getSpecialValue()));
			return;
		}
		Value v = data != null ? data.getValue() : null;
		if (v == null) {
			out.append("null");
			return;
		}
		if (v.isCollection()) {
			out.append('[');
			List<Data<Type, Value>> items = v.getItems();
			for (int i = 0; i < items.size(); i++) {
				if (i > 0)
					out.append(", ");
				render(items.get(i), template.getItem(i), out);
			}
			out.append(']');
			return;
		}
		if (v.isStructure()) {
			out.append('{');
			boolean first = true;
			for (String name : v.getParameters()) {
				if (!first)
					out.append(", ");
				first = false;
				out.append(name).append(": ");
				render(v.getParameter(name), template.getMember(name), out);
			}
			out.append('}');
			return;
		}
		Object primitive = v.getValue();
		if (primitive instanceof CharSequence || primitive instanceof Character)
			out.append('"').append(primitive).append('"');
		else if (primitive != null)
			out.append(primitive);
		else
			out.append(v.getQualifiedName() != null ? v.getQualifiedName() : "<value>");
	}

}
+80 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.rt.core;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.etsi.mts.tdl.execution.java.tri.Data;
import org.etsi.mts.tdl.execution.java.tri.Reporter;

/**
 * Keys of the properties the execution engine passes to
 * {@link Reporter#behaviourStarted(String, String, String, Map)} and
 * {@link Reporter#behaviourCompleted(String, String, Map)}.
 * <p>
 * Values are plain Java values: {@link String}, {@link Boolean},
 * {@link Number}, {@link Data}, or arrays/lists of those. No TDL model objects
 * are passed.
 * <p>
 * Properties reported at start describe the behaviour as specified and
 * properties reported at completion describe its outcome. A property is only
 * present if applicable to the kind of behaviour and known at the time.
 */
public final class BehaviourProperties {

	private BehaviourProperties() {
	}

	/**
	 * Builds a property map from alternating keys and values, <b>null</b> values
	 * are allowed and insertion order is kept.
	 */
	public static Map<String, Object> of(Object... keyValues) {
		if (keyValues == null || keyValues.length == 0)
			return Collections.emptyMap();
		if (keyValues.length % 2 != 0)
			throw new IllegalArgumentException("Properties must be given as key/value pairs");
		Map<String, Object> properties = new LinkedHashMap<>();
		for (int i = 0; i < keyValues.length; i += 2) {
			if (!(keyValues[i] instanceof String))
				throw new IllegalArgumentException("Property key must be a String: " + keyValues[i]);
			properties.put((String) keyValues[i], keyValues[i + 1]);
		}
		return Collections.unmodifiableMap(properties);
	}

	// ---- Any behaviour (start)

	/**
	 * Location of the behaviour in the TDL source, as <code>path:line</code>.
	 * Reported if source traces are enabled in the code generator.
	 */
	public static final String SOURCE = "source";


	// ---- Assertion (completion)

	/**
	 * {@link Boolean}: whether the condition evaluated to true.
	 */
	public static final String RESULT = "result";

	/**
	 * {@link String}: name of the top-level operator of the condition (e.g.
	 * <code>==</code>), if the condition is an operator application.
	 */
	public static final String OPERATOR = "operator";

	/**
	 * <code>Object[]</code>: the evaluated operands of the top-level operator, in
	 * order; reported together with {@link #OPERATOR}.
	 */
	public static final String OPERANDS = "operands";

	/**
	 * The evaluated value of the condition, if it is not an operator application
	 * (otherwise see {@link #OPERATOR}/{@link #OPERANDS}).
	 */
	public static final String VALUE = "value";

}
+5 −0
Original line number Diff line number Diff line
@@ -414,6 +414,11 @@ public class TestControl {
		return c;
	}

	private void log(String message) {
		if (reporter != null)
			reporter.log(getTesterComponent() != null ? getTesterComponent().getName() : null, message);
	}

	public ExecutionCallable timeout(Timer timer) {
		ExecutionCallable c = new ExecutionCallable() {
			@Override
Loading