Commit 09809c24 authored by Martti Käärik's avatar Martti Käärik
Browse files

Template-aware matcher for pojos and un-mapped values #114

parent e2191972
Loading
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -2199,6 +2199,8 @@ public class JUnitTestGenerator extends Renderer {

				if (isUnmapped()) {
					line(unmappedDataInitializer);
					// The TDL identity of the instance; the mapping is for the adapter
					writeSetName(dataInstance);
					append(".setMapping(");
					writeMapping(m, ref + "_mapping", true);
					line(")");
@@ -2498,6 +2500,10 @@ public class JUnitTestGenerator extends Renderer {
			UnassignedMemberTreatment t = getUnassignedMember(dataInstance, tdlPackage.eINSTANCE.getStructuredDataInstance_UnassignedMember());
			return isUndefined(t) ? null : t;
		}
		if (dataInstance instanceof CollectionDataInstance) {
			UnassignedMemberTreatment t = getUnassignedMember(dataInstance, tdlPackage.eINSTANCE.getCollectionDataInstance_UnassignedMember());
			return isUndefined(t) ? null : t;
		}
		return null;
	}

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

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.SpecialValue;
import org.etsi.mts.tdl.execution.java.tri.Template;
import org.etsi.mts.tdl.execution.java.tri.Validator;
import org.etsi.mts.tdl.execution.java.tri.Verdict;

/**
 * Common part of the default validators: the interpretation of the expected
 * data template (see {@link Data#getTemplate()} and {@link Template}).
 * Subclasses supply the navigation of a particular data representation and
 * apply, for each part of the expected data:
 * <ol>
 * <li>a special value marking of the template node, regardless of the expected
 * value at that position: <i>AnyValue</i> requires a present value,
 * <i>AnyValueOrOmit</i> matches anything, <i>OmitValue</i> requires an absent
 * value (see {@link #matchesSpecial(boolean, Template, String)});</li>
 * <li>otherwise a concrete comparison if the expected value is present;</li>
 * <li>otherwise the treatment of unassigned members in effect for the
 * containing node, or {@link #setDefaultUnassignedMemberTreatment(SpecialValue)
 * the default treatment} (<i>AnyValueOrOmit</i> unless configured otherwise) if
 * none is defined (see
 * {@link #matchesUnassigned(boolean, Template, String)}).</li>
 * </ol>
 * The first mismatch found is reported through
 * {@link #reportMismatch(String, String)}. Verdict handling is left to
 * subclasses; {@link #setVerdict(Verdict)} only logs.
 */
public abstract class AbstractTemplateValidator implements Validator {

	/**
	 * Treatment applied to structure members and collection items that are neither
	 * assigned in the expected value nor marked in its template (i.e. unassigned at
	 * TDL level with the <i>unassignedMember</i> property undefined).
	 */
	private SpecialValue defaultUnassignedMemberTreatment = SpecialValue.AnyValueOrOmit;

	public void setDefaultUnassignedMemberTreatment(SpecialValue treatment) {
		if (treatment == SpecialValue.OmitValue)
			throw new IllegalArgumentException("OmitValue is not a valid unassigned member treatment");
		this.defaultUnassignedMemberTreatment = treatment;
	}

	public SpecialValue getDefaultUnassignedMemberTreatment() {
		return defaultUnassignedMemberTreatment;
	}

	private Reporter reporter;

	/**
	 * Set the reporter to log mismatches through (see
	 * {@link #reportMismatch(String, String)}).
	 */
	public void setReporter(Reporter reporter) {
		this.reporter = reporter;
	}

	/**
	 * Reports the part of the expected data at which matching failed. Matching
	 * stops at the first mismatch, so this is called at most once per
	 * {@link #matches(Data, Data)} call. The default implementation logs at
	 * {@link LogLevel#Debug} through the {@link #setReporter(Reporter) reporter} if
	 * one is set (reporting as "Validator", the tester component not being known
	 * here), and prints to the console otherwise.
	 *
	 * @param path   Path of the part within the expected data: member names
	 *               separated by dots, item indexes in brackets; empty for the
	 *               root.
	 * @param reason What was expected and what was found.
	 */
	protected void reportMismatch(String path, String reason) {
		String message = "Mismatch at '" + (path.isEmpty() ? "<root>" : path) + "': " + reason;
		if (reporter != null)
			reporter.log(null, "Validator | " + message);
		else
			System.out.println(message);
	}

	/**
	 * Reports a mismatch and returns <b>false</b>, for use in match expressions.
	 */
	protected final boolean mismatch(String path, String reason) {
		reportMismatch(path, reason);
		return false;
	}

	protected static String memberPath(String path, String name) {
		return path.isEmpty() ? name : path + "." + name;
	}

	protected static String itemPath(String path, int index) {
		return path + "[" + index + "]";
	}

	/**
	 * Match a part marked as a special value in the template against the presence
	 * of the actual part. Only call if {@link Template#getSpecialValue()} of the
	 * node is non-null.
	 *
	 * @param actualPresent Whether the actual part is present (non-null).
	 * @param template      The template node of the part.
	 * @param path          Path of the part, for mismatch reporting.
	 */
	protected boolean matchesSpecial(boolean actualPresent, Template template, String path) {
		SpecialValue marking = template.getSpecialValue();
		if (marking == SpecialValue.AnyValue)
			return actualPresent || mismatch(path, "expected: any value, actual: omit");
		if (marking == SpecialValue.AnyValueOrOmit)
			return true;
		// OmitValue
		return !actualPresent || mismatch(path, "expected: omit, actual: present");
	}

	/**
	 * Match a member/item that is unassigned at TDL level (absent expected value,
	 * no special value marking), using the treatment of unassigned members in
	 * effect for the containing node or the default treatment.
	 *
	 * @param actualPresent Whether the actual part is present (non-null).
	 * @param container     The template node of the containing structure or
	 *                      collection.
	 * @param path          Path of the part, for mismatch reporting.
	 */
	protected boolean matchesUnassigned(boolean actualPresent, Template container, String path) {
		SpecialValue treatment = container.getUnassignedMemberTreatment();
		if (treatment == null)
			treatment = defaultUnassignedMemberTreatment;
		if (treatment == SpecialValue.AnyValue)
			return actualPresent || mismatch(path, "unassigned member treated as any value, actual omit");
		return true;
	}

	@Override
	public void setVerdict(Verdict verdict) {
		if (reporter != null)
			reporter.log(null, "Validator | " + "Set verdict to '" + verdict.getName() + "'");
		else
			System.out.println("Set verdict to " + verdict.getName());
	}

}
+43 −4
Original line number Diff line number Diff line
@@ -5,8 +5,43 @@ import org.etsi.mts.tdl.execution.java.tri.Reporter;
import org.etsi.mts.tdl.execution.java.tri.Validator;
import org.etsi.mts.tdl.execution.java.tri.Verdict;

/**
 * Default all-in-one adapter: a {@link Reporter} that logs to the console and
 * a {@link Validator} that delegates matching to a configurable validator —
 * {@link PojoValidator} for POJO mapped data (the default) or
 * {@link ValueValidator} for unmapped
 * {@link org.etsi.mts.tdl.execution.java.tri.Value Value} data.
 */
public class DefaultAdapter implements Validator, Reporter {

	private Validator validator;

	/**
	 * Uses {@link PojoValidator}.
	 */
	public DefaultAdapter() {
		this(new PojoValidator());
	}

	/**
	 * @param validator The validator to delegate matching to.
	 */
	public DefaultAdapter(Validator validator) {
		setValidator(validator);
	}

	public Validator getValidator() {
		return validator;
	}

	public DefaultAdapter setValidator(Validator validator) {
		this.validator = validator;
		if (validator instanceof AbstractTemplateValidator)
			// Route mismatch reports through this reporter
			((AbstractTemplateValidator) validator).setReporter(this);
		return this;
	}

	public boolean equals(Object o0, Object o1) {
		// TODO Auto-generated method stub
		return false;
@@ -23,6 +58,12 @@ public class DefaultAdapter implements Validator, Reporter {
		System.out.println("[" + tester + "] " + body);
	}

	@Override
	public void log(String tester, String message) {
		String line = "[" + (tester != null ? tester : "-") + "] " + message;
		System.out.println(line);
	}

	@Override
	public void testObjectiveReached(String tester, String uri, String description) {
		// TODO Auto-generated method stub
@@ -49,14 +90,12 @@ public class DefaultAdapter implements Validator, Reporter {

	@Override
	public boolean matches(Data expected, Data actual) {
		// TODO Auto-generated method stub
		return false;
		return validator.matches(expected, actual);
	}

	@Override
	public void setVerdict(Verdict verdict) {
		// TODO Auto-generated method stub
		System.out.println("Set verdict to " + verdict.getName());
		validator.setVerdict(verdict);
	}

}
+3 −9
Original line number Diff line number Diff line
@@ -2,7 +2,6 @@ package org.etsi.mts.tdl.execution.java.adapters;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.etsi.mts.tdl.execution.java.rt.core.TemplateImpl;
import org.etsi.mts.tdl.execution.java.tri.Data;
@@ -56,7 +55,7 @@ public final class PojoRenderer {
			out.append('"').append(value).append('"');
			return;
		}
		if (PojoReflection.isLeaf(value) && !(value instanceof List) && !(value instanceof Map)) {
		if (PojoReflection.isLeaf(value) && !(value instanceof List)) {
			out.append(value);
			return;
		}
@@ -73,13 +72,8 @@ public final class PojoRenderer {
		}
		out.append('{');
		boolean first = true;
		if (value instanceof Map) {
			for (Map.Entry<?, ?> e : ((Map<?, ?>) value).entrySet())
				first = renderNamed(String.valueOf(e.getKey()), e.getValue(), template, first, out);
		} else {
		for (PojoReflection.Member m : PojoReflection.members(value.getClass()))
			first = renderNamed(m.getName(), m.read(value), template, first, out);
		}
		out.append('}');
	}

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

import java.util.Iterator;
import java.util.List;

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.Validator;

/**
 * Validator for mapped data. Compares the expected and actual object graphs
 * structurally by reflection: JDK types and enums by <code>equals()</code>,
 * lists item by item with equal size, maps by the expected keys, other objects
 * field by field including superclasses (read through the JavaBeans getter of a
 * field if the class provides one, directly otherwise). Template markings and
 * unassigned member treatment are applied as described in
 * {@link AbstractTemplateValidator}; template members are navigated by Java
 * field/property name.
 */
public class PojoValidator extends AbstractTemplateValidator {

	@Override
	public boolean matches(Data expected, Data actual) {
		if (expected == null)
			return true;
		Object actualValue = actual != null ? actual.getValue() : null;
		return matches(expected.getValue(), actualValue, expected.getTemplate(), "");
	}

	private boolean matches(Object expected, Object actual, Template template, String path) {
		if (template.getSpecialValue() != null)
			return matchesSpecial(actual != null, template, path);
		if (expected == null)
			return actual == null || mismatch(path, "expected: omit, actual: " + describe(actual));
		if (actual == null)
			return mismatch(path, "expected: " + describe(expected) + ", actual: omit");

		if (expected instanceof List)
			return matchesItems((List<?>) expected, actual, template, path);

		if (PojoReflection.isLeaf(expected))
			return expected.equals(actual)
					|| mismatch(path, "expected: " + describe(expected) + ", actual: " + describe(actual));

		return matchesFields(expected, actual, template, path);
	}

	private boolean matchesItems(List<?> expected, Object actual, Template template, String path) {
		if (!(actual instanceof List))
			return mismatch(path, "expected: a list, actual: " + actual.getClass().getName());
		List<?> actualItems = (List<?>) actual;
		// TODO temporarily disabled size matching of collections
//		if (expected.size() != actualItems.size())
//			return mismatch(path, "expected: " + expected.size() + " items, actual: " + actualItems.size());
		Iterator<?> e = expected.iterator();
		Iterator<?> a = actualItems.iterator();
		for (int i = 0; e.hasNext(); i++) {
			Object expectedItem = e.next();
			// A missing actual item counts as absent
			Object actualItem = a.hasNext() ? a.next() : null;
			Template itemTemplate = template.getItem(i);
			String itemPath = itemPath(path, i);
			if (expectedItem == null && itemTemplate.getSpecialValue() == null) {
				// Unassigned at TDL level
				if (!matchesUnassigned(actualItem != null, template, itemPath))
					return false;
			} else if (!matches(expectedItem, actualItem, itemTemplate, itemPath))
				return false;
		}
		return true;
	}

	private boolean matchesFields(Object expected, Object actual, Template template, String path) {
		if (!expected.getClass().isAssignableFrom(actual.getClass()))
			return mismatch(path,
					"expected: " + expected.getClass().getName() + ", actual: " + actual.getClass().getName());
		for (PojoReflection.Member m : PojoReflection.members(expected.getClass()))
			if (!matchesMember(m.getName(), m.read(expected), m.read(actual), template, path))
				return false;
		return true;
	}

	private boolean matchesMember(String name, Object expectedMember, Object actualMember, Template template,
			String path) {
		Template memberTemplate = template.getMember(name);
		String memberPath = memberPath(path, name);
		if (expectedMember == null && memberTemplate.getSpecialValue() == null)
			// Unassigned at TDL level
			return matchesUnassigned(actualMember != null, template, memberPath);
		return matches(expectedMember, actualMember, memberTemplate, memberPath);
	}

	private String describe(Object value) {
		return PojoRenderer.render(value, null);
	}

}
Loading