Loading plugins/org.etsi.mts.tdl.execution.java.codegen/src/org/etsi/mts/tdl/execution/java/codegen/JUnitTestGenerator.java +120 −24 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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>(); Loading @@ -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; Loading Loading @@ -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 + ".*"); Loading Loading @@ -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). Loading Loading @@ -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("); Loading @@ -1136,7 +1187,7 @@ public class JUnitTestGenerator extends Renderer { append("VerdictImpl.fail"); line(");"); writeAssertionFailure(b, ((Assertion) b).getCondition(), dataUseVariables); writeAssertionFailure(b, condition, dataUseVariables); blockClose(); Loading Loading @@ -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; Loading Loading @@ -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) { Loading Loading @@ -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(); Loading @@ -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; Loading plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/adapters/DefaultAdapter.java +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; Loading Loading @@ -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 Loading plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/adapters/ValueRenderer.java 0 → 100644 +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>"); } } plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/rt/core/BehaviourProperties.java 0 → 100644 +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"; } plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/rt/core/TestControl.java +5 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
plugins/org.etsi.mts.tdl.execution.java.codegen/src/org/etsi/mts/tdl/execution/java/codegen/JUnitTestGenerator.java +120 −24 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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>(); Loading @@ -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; Loading Loading @@ -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 + ".*"); Loading Loading @@ -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). Loading Loading @@ -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("); Loading @@ -1136,7 +1187,7 @@ public class JUnitTestGenerator extends Renderer { append("VerdictImpl.fail"); line(");"); writeAssertionFailure(b, ((Assertion) b).getCondition(), dataUseVariables); writeAssertionFailure(b, condition, dataUseVariables); blockClose(); Loading Loading @@ -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; Loading Loading @@ -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) { Loading Loading @@ -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(); Loading @@ -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; Loading
plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/adapters/DefaultAdapter.java +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; Loading Loading @@ -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 Loading
plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/adapters/ValueRenderer.java 0 → 100644 +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>"); } }
plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/rt/core/BehaviourProperties.java 0 → 100644 +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"; }
plugins/org.etsi.mts.tdl.execution.java.runtime/src/org/etsi/mts/tdl/execution/java/rt/core/TestControl.java +5 −0 Original line number Diff line number Diff line Loading @@ -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