From 4e0ee4bc84b73ac053672b0b994a267c77146ab4 Mon Sep 17 00:00:00 2001 From: Rene Schneider Date: Thu, 21 Feb 2019 09:30:26 +0100 Subject: [PATCH] extended support for inexistent results (issue #170) to nested objects --- .../AbstractModularValueConverter.java | 55 ++++++++++-------- .../conversion/ConversionContext.java | 24 +++++++- .../conversion/InexistentValueHandling.java | 37 ++++++++++++ .../comparator/DefaultResultComparator.java | 56 ++++++++++++++----- .../basic/beans/simpleBeanTest.integrity | 22 ++++++++ .../junit/basic/beans/SimpleBeanTest.java | 17 +++++- ...ic.beans.simpleBeanWithInexistenceTest.xml | 36 ++++++++++++ 7 files changed, 210 insertions(+), 37 deletions(-) create mode 100644 de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/InexistentValueHandling.java create mode 100644 de.gebit.integrity.tests/results/integrity.basic.beans.simpleBeanWithInexistenceTest.xml diff --git a/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/AbstractModularValueConverter.java b/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/AbstractModularValueConverter.java index ade381182..49d10ddc3 100644 --- a/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/AbstractModularValueConverter.java +++ b/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/AbstractModularValueConverter.java @@ -31,6 +31,7 @@ import de.gebit.integrity.dsl.ConstantDefinition; import de.gebit.integrity.dsl.ConstantValue; import de.gebit.integrity.dsl.CustomOperation; +import de.gebit.integrity.dsl.InexistentValue; import de.gebit.integrity.dsl.StandardOperation; import de.gebit.integrity.dsl.StringValue; import de.gebit.integrity.dsl.ValueOrEnumValueOrOperation; @@ -99,23 +100,27 @@ public abstract class AbstractModularValueConverter implements ValueConverter { /** * All known conversions. */ - private final Map>> conversions = new HashMap>>(); + private final Map>> conversions + = new HashMap>>(); /** * Conversions derived from the directly added conversions by searching superclasses of the target type. */ - private final Map>>> derivedConversions = new HashMap>>>(); + private final Map>>> derivedConversions + = new HashMap>>>(); /** * Reverse index of all known conversions. */ - private final Map>, ConversionKey> conversionToKey = new HashMap>, ConversionKey>(); + private final Map>, ConversionKey> conversionToKey + = new HashMap>, ConversionKey>(); /** * The default conversions for all known source types. These are the conversions with the highest priority from * their respective source types' conversion pool. */ - private final Map, Class>> defaultConversions = new HashMap, Class>>(); + private final Map, Class>> defaultConversions + = new HashMap, Class>>(); /** * The current defaults' priority. Used to fill the {@link #defaultConversions} map. @@ -368,6 +373,12 @@ private Object convertSingleValueToTargetType(Class aTargetType, Class aPa return null; } + if (aValue instanceof InexistentValue + && aConversionContext.getInexistentValueHandlingPolicy() == InexistentValueHandling.KEEP_AS_IS) { + // In case of an Inexistent value, we may have to keep it as-is, if required by conversion context policy + return aValue; + } + // It is possible that we arrive here with a single value, but still a target type that's an array. In that case // we need to create the requested array, but it will only have one element. if (aTargetType != null && aTargetType.isArray()) { @@ -500,8 +511,8 @@ protected Object convertEncapsulatedValueToTargetType(Class aTargetType, Clas // cannot execute operations without the ability to load them return null; } else { - CustomOperationWrapper tempWrapper = wrapperFactory - .newCustomOperationWrapper((CustomOperation) aValue); + CustomOperationWrapper tempWrapper + = wrapperFactory.newCustomOperationWrapper((CustomOperation) aValue); Object tempResult = tempWrapper.executeOperation(); return convertPlainValueToTargetType(aTargetType, aParameterizedType, tempResult, aConversionContext, someVisitedValues); @@ -671,8 +682,8 @@ protected Object convertEncapsulatedValueCollectionToTargetType(Class aTarget // that property by the position within the array, and reset it later to the original plain value. This // is necessary to distinguish between array elements when converting to formatted strings (there's // highlighting being added based on these paths). In all other conversion cases it at least doesn't hurt. - String tempBaseObjectPath = (String) aConversionContext - .getProperty(AbstractNestedObjectToString.NESTEDOBJECT_PATH_PROPERTY); + String tempBaseObjectPath + = (String) aConversionContext.getProperty(AbstractNestedObjectToString.NESTEDOBJECT_PATH_PROPERTY); Object tempResultArray = Array.newInstance(tempTargetArrayType, aCollection.getMoreValues().size() + 1); for (int i = 0; i < aCollection.getMoreValues().size() + 1; i++) { @@ -681,8 +692,8 @@ protected Object convertEncapsulatedValueCollectionToTargetType(Class aTarget tempBaseObjectPath + "#" + i); } - ValueOrEnumValueOrOperation tempValue = (i == 0 ? aCollection.getValue() - : aCollection.getMoreValues().get(i - 1)); + ValueOrEnumValueOrOperation tempValue + = (i == 0 ? aCollection.getValue() : aCollection.getMoreValues().get(i - 1)); Object tempResultValue = convertEncapsulatedValueToTargetType(tempTargetType, aParameterizedType, tempValue, aConversionContext, someVisitedValues); Array.set(tempResultArray, i, tempResultValue); @@ -810,8 +821,8 @@ public FormattedString convertValueToFormattedString(Object aValue, boolean aFor @Override public String[] convertValueToStringArray(Object aValue, ConversionContext aConversionContext) { - FormattedString[] tempFormattedStrings = convertValueToStringArray(aValue, aConversionContext, - new HashSet()); + FormattedString[] tempFormattedStrings + = convertValueToStringArray(aValue, aConversionContext, new HashSet()); String[] tempStrings = new String[tempFormattedStrings.length]; for (int i = 0; i < tempFormattedStrings.length; i++) { @@ -1080,8 +1091,8 @@ public String toString() { protected Conversion findAndInstantiateConversion(Class aSourceType, Class aTargetType, Set someVisitedValues, ConversionContext aConversionContext) throws InstantiationException, IllegalAccessException { - Class> tempConversionClass = findConversion(aSourceType, aTargetType, - someVisitedValues, aConversionContext); + Class> tempConversionClass + = findConversion(aSourceType, aTargetType, someVisitedValues, aConversionContext); return createConversionInstance(tempConversionClass, someVisitedValues); } @@ -1100,8 +1111,8 @@ public String toString() { */ protected Class> findConversion(Class aSourceType, Class aTargetType, Set someVisitedValues, ConversionContext aConversionContext) { - Class> tempConversion = findConversionRecursive(aSourceType, aTargetType, - aConversionContext); + Class> tempConversion + = findConversionRecursive(aSourceType, aTargetType, aConversionContext); if (tempConversion != null) { return tempConversion; @@ -1154,8 +1165,8 @@ public String toString() { * @return a conversion class, or null if none was found */ protected Class> searchDerivedConversionMap(Class aSourceType, Class aTargetType) { - List>> tempList = derivedConversions - .get(new ConversionKey(aSourceType, aTargetType)); + List>> tempList + = derivedConversions.get(new ConversionKey(aSourceType, aTargetType)); if (tempList != null && !tempList.isEmpty()) { return tempList.get(0); } @@ -1221,15 +1232,15 @@ public String toString() { for (Class tempSourceInterface : tempSourceTypeInFocus.getInterfaces()) { if (aTargetType == null || aTargetType == Object.class) { // This is the default target type case - Class> tempConversion = findConversionRecursive(tempSourceInterface, - null, aConversionContext); + Class> tempConversion + = findConversionRecursive(tempSourceInterface, null, aConversionContext); if (tempConversion != null) { return tempConversion; } } else { // We actually have a target type - Class> tempConversion = findConversionRecursive(tempSourceInterface, - aTargetType, aConversionContext); + Class> tempConversion + = findConversionRecursive(tempSourceInterface, aTargetType, aConversionContext); if (tempConversion != null) { return tempConversion; } diff --git a/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/ConversionContext.java b/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/ConversionContext.java index 5caaeb90b..715f47e47 100644 --- a/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/ConversionContext.java +++ b/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/ConversionContext.java @@ -11,6 +11,7 @@ import java.util.Map; import de.gebit.integrity.comparator.ComparisonResult; +import de.gebit.integrity.dsl.InexistentValue; /** * A conversion context is a container for contextual information required to perform a value conversion.
@@ -33,7 +34,13 @@ public class ConversionContext implements Cloneable { /** * The way in which unresolvable variables shall be treated. */ - protected UnresolvableVariableHandling unresolvableVariableHandlingPolicy = UnresolvableVariableHandling.RESOLVE_TO_NULL_VALUE; + protected UnresolvableVariableHandling unresolvableVariableHandlingPolicy + = UnresolvableVariableHandling.RESOLVE_TO_NULL_VALUE; + + /** + * The way in which {@link InexistentValue}s are to be treated. + */ + protected InexistentValueHandling inexistentValueHandlingPolicy = InexistentValueHandling.CONVERT; /** * In case of a value being converted which belongs to a comparison that has been executed, the result of said @@ -76,6 +83,17 @@ public ConversionContext withUnresolvableVariableHandlingPolicy(UnresolvableVari return this; } + /** + * Enables a certain {@link InexistentValueHandling} policy instead of the default. + * + * @param aPolicy + * the policy to use + */ + public ConversionContext withInexistentValueHandling(InexistentValueHandling aPolicy) { + inexistentValueHandlingPolicy = aPolicy; + return this; + } + /** * Adds the provided comparison result. * @@ -108,6 +126,10 @@ public UnresolvableVariableHandling getUnresolvableVariableHandlingPolicy() { return unresolvableVariableHandlingPolicy; } + public InexistentValueHandling getInexistentValueHandlingPolicy() { + return inexistentValueHandlingPolicy; + } + public ComparisonResult getComparisonResult() { return comparisonResult; } diff --git a/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/InexistentValueHandling.java b/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/InexistentValueHandling.java new file mode 100644 index 000000000..133a8a12d --- /dev/null +++ b/de.gebit.integrity.dsl/src/de/gebit/integrity/parameter/conversion/InexistentValueHandling.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2019 Rene Schneider, GEBIT Solutions GmbH and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package de.gebit.integrity.parameter.conversion; + +import de.gebit.integrity.dsl.InexistentValue; +import de.gebit.integrity.utils.ParameterUtil; + +/** + * This enum offers various ways in which inexistent values ({@link InexistentValue}) may be handled during conversion. + * + * @author Rene Schneider - initial API and implementation + * + */ +public enum InexistentValueHandling { + + /** + * Inexistent values should be converted normally - which most likely means they will end up as a {@link String}. + * However, the String won't be just any object, but the specific, reserved instance + * {@link ParameterUtil#INEXISTENT_VALUE} (so even if converted, one could perform an instance comparison to + * specifically detect inexistent values - however, this is discouraged and should only be used if it is not + * possible to simply turn off conversion of inexistent values via {@link #KEEP_AS_IS}). + *

+ * This is the default if not specified otherwise in a {@link ConversionContext}. + */ + CONVERT, + + /** + * Inexistent values will not be converted. + */ + KEEP_AS_IS; + +} diff --git a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/comparator/DefaultResultComparator.java b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/comparator/DefaultResultComparator.java index 9c67e3cd6..57b83c75e 100644 --- a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/comparator/DefaultResultComparator.java +++ b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/comparator/DefaultResultComparator.java @@ -29,6 +29,7 @@ import de.gebit.integrity.comparator.SimpleComparisonResult; import de.gebit.integrity.dsl.CustomOperation; import de.gebit.integrity.dsl.DateValue; +import de.gebit.integrity.dsl.InexistentValue; import de.gebit.integrity.dsl.MethodReference; import de.gebit.integrity.dsl.NestedObject; import de.gebit.integrity.dsl.NullValue; @@ -40,6 +41,8 @@ import de.gebit.integrity.dsl.VariableOrConstantEntity; import de.gebit.integrity.fixtures.FixtureWrapper; import de.gebit.integrity.operations.UnexecutableException; +import de.gebit.integrity.parameter.conversion.ConversionContext; +import de.gebit.integrity.parameter.conversion.InexistentValueHandling; import de.gebit.integrity.parameter.conversion.UnresolvableVariableHandling; import de.gebit.integrity.parameter.conversion.ValueConverter; import de.gebit.integrity.parameter.resolving.ParameterResolver; @@ -83,8 +86,8 @@ public ComparisonResult compareResult(Object aFixtureResult, ValueOrEnumValueOrO tempIsNull = true; } else { // ...or indirectly by the value being a variable/constant that resolves to a null value - VariableOrConstantEntity tempEntity = IntegrityDSLUtil - .extractVariableOrConstantEntity(anExpectedResult.getValue()); + VariableOrConstantEntity tempEntity + = IntegrityDSLUtil.extractVariableOrConstantEntity(anExpectedResult.getValue()); if (tempEntity != null) { Object tempResult = parameterResolver.resolveParameterValue(anExpectedResult, UnresolvableVariableHandling.RESOLVE_TO_NULL_VALUE); @@ -105,18 +108,18 @@ public ComparisonResult compareResult(Object aFixtureResult, ValueOrEnumValueOrO tempConversionTargetType = aFixtureInstance.determineCustomConversionTargetType(aFixtureResult, tempMethodName, aPropertyName); } else { - tempConversionTargetType = aFixtureResult.getClass().isArray() - ? aFixtureResult.getClass().getComponentType() - : aFixtureResult.getClass(); + tempConversionTargetType + = aFixtureResult.getClass().isArray() ? aFixtureResult.getClass().getComponentType() + : aFixtureResult.getClass(); } if (anExpectedResult.getMoreValues().size() > 0) { // multiple result values given -> we're going to put them into an array of the same type // as the fixture result - Class tempArrayType = (tempConversionTargetType == null) ? Object.class - : tempConversionTargetType; - tempConvertedResult = Array.newInstance(tempArrayType, - anExpectedResult.getMoreValues().size() + 1); + Class tempArrayType + = (tempConversionTargetType == null) ? Object.class : tempConversionTargetType; + tempConvertedResult + = Array.newInstance(tempArrayType, anExpectedResult.getMoreValues().size() + 1); for (int i = 0; i < Array.getLength(tempConvertedResult); i++) { ValueOrEnumValueOrOperation tempSingleExpectedResult = (i == 0 ? anExpectedResult.getValue() : anExpectedResult.getMoreValues().get(i - 1)); @@ -274,7 +277,10 @@ protected ComparisonResult convertAndPerformEqualityCheck(Object aSingleFixtureR } tempConvertedFixtureResult = valueConverter.convertValue(Map.class, aSingleFixtureResult, null); - tempConvertedExpectedResult = valueConverter.convertValue(Map.class, tempNestedObject, null); + // Keeping Inexistent values as they are (= the InexistentValue model class instance) is necessary so that + // we can check for them later in map comparison. Otherwise they would be converted to strings. + tempConvertedExpectedResult = valueConverter.convertValue(Map.class, tempNestedObject, + new ConversionContext().withInexistentValueHandling(InexistentValueHandling.KEEP_AS_IS)); } else { if (tempSingleExpectedResult instanceof Map && !(aSingleFixtureResult instanceof Map)) { // if the expected result is a map, and the fixture has NOT returned a map, we also assume the fixture @@ -283,8 +289,8 @@ protected ComparisonResult convertAndPerformEqualityCheck(Object aSingleFixtureR tempConvertedExpectedResult = tempSingleExpectedResult; } else { // no special bean-related cases apply: convert the expected result to match the given fixture result - tempConvertedExpectedResult = valueConverter.convertValue(aConversionTargetType, - tempSingleExpectedResult, null); + tempConvertedExpectedResult + = valueConverter.convertValue(aConversionTargetType, tempSingleExpectedResult, null); } } @@ -375,7 +381,8 @@ protected ComparisonResult performEqualityCheck(Object aConvertedResult, Object /** * Compare two {@link Map}s for equality. Maps are considered equal if all the values in the expected result are - * found in the actual result (there may well be more keys in the actual result than expected!). + * found in the actual result (there may well be more keys in the actual result than expected, except if one of the + * maps declares one of these "inexistent", in which case it may NOT exist in the other!). * * @param aResult * the result returned by the fixture @@ -394,6 +401,18 @@ protected MapComparisonResult performEqualityCheckForMaps(Map aResult, Map Object tempActualValue = ((Map) aResult).get(tempEntry.getKey()); Object tempReferenceValue = tempEntry.getValue(); + if (tempReferenceValue instanceof InexistentValue) { + if (tempActualValue != null || ((Map) aResult).containsKey(tempEntry.getKey())) { + // Reference values requires inexistence of key in actual map, but it contains it -> failure + tempSuccess = false; + tempCombinedFailedPaths.add(tempEntry.getKey().toString()); + continue; + } else { + // Inexistence of value confirmed -> prevent further comparison (which would fail) + continue; + } + } + Object tempConvertedReferenceValue = tempReferenceValue; if (!(tempActualValue instanceof Map && tempReferenceValue instanceof Map)) { // If the inner values aren't maps themselves, special handling is required. @@ -511,6 +530,17 @@ protected ComparisonResult performEqualityCheckForDates(Date aResult, Date anExp } } + /** + * Compare two {@link Temporal}s for equality. + * + * @param aResult + * the result returned by the fixture + * @param anExpectedResult + * the expected result as in the script, converted for comparison + * @param aRawExpectedResult + * the raw expected result as in the script, before conversion + * @return true if equal, false otherwise + */ protected ComparisonResult performEqualityCheckForJava8Dates(Temporal aResult, Temporal anExpectedResult, Object aRawExpectedResult) { if (aRawExpectedResult instanceof DateValue) { diff --git a/de.gebit.integrity.tests/integrity/suites/basic/beans/simpleBeanTest.integrity b/de.gebit.integrity.tests/integrity/suites/basic/beans/simpleBeanTest.integrity index 6961b40b5..187368c34 100644 --- a/de.gebit.integrity.tests/integrity/suites/basic/beans/simpleBeanTest.integrity +++ b/de.gebit.integrity.tests/integrity/suites/basic/beans/simpleBeanTest.integrity @@ -23,5 +23,27 @@ packagedef integrity.basic.beans with } suiteend + + suitedef simpleBeanWithInexistenceTest with + + -- This should succeed + test createSimpleBeanUntyped = { + inventedParameter: inexistent + secondParameter: 100 + thirdParameter: { + innerParameter: 1.99 + } + } + + -- This should fail + test createSimpleBeanUntyped = { + firstParameter: inexistent + secondParameter: 100 + thirdParameter: { + innerParameter: 1.99 + } + } + + suiteend packageend \ No newline at end of file diff --git a/de.gebit.integrity.tests/junit/de/gebit/integrity/tests/junit/basic/beans/SimpleBeanTest.java b/de.gebit.integrity.tests/junit/de/gebit/integrity/tests/junit/basic/beans/SimpleBeanTest.java index 410c19e05..8d41344c1 100644 --- a/de.gebit.integrity.tests/junit/de/gebit/integrity/tests/junit/basic/beans/SimpleBeanTest.java +++ b/de.gebit.integrity.tests/junit/de/gebit/integrity/tests/junit/basic/beans/SimpleBeanTest.java @@ -33,11 +33,26 @@ public class SimpleBeanTest extends IntegrityJUnitTest { * @throws JDOMException */ @Test - public void test() throws ModelLoadException, IOException, JDOMException { + public void testSimpleBean() throws ModelLoadException, IOException, JDOMException { Document tempResult = executeIntegritySuite( new String[] { "integrity/suites/basic/beans/simpleBeanTest.integrity" }, "integrity.basic.beans.simpleBeanTest", null); assertDocumentMatchesReference(tempResult); } + /** + * Performs a suite which does fixture calls with bean values and checks the resulting XML document. + * + * @throws ModelLoadException + * @throws IOException + * @throws JDOMException + */ + @Test + public void testSimpleBeanWithInexistence() throws ModelLoadException, IOException, JDOMException { + Document tempResult = executeIntegritySuite( + new String[] { "integrity/suites/basic/beans/simpleBeanTest.integrity" }, + "integrity.basic.beans.simpleBeanWithInexistenceTest", null); + assertDocumentMatchesReference(tempResult); + } + } diff --git a/de.gebit.integrity.tests/results/integrity.basic.beans.simpleBeanWithInexistenceTest.xml b/de.gebit.integrity.tests/results/integrity.basic.beans.simpleBeanWithInexistenceTest.xml new file mode 100644 index 000000000..80567981f --- /dev/null +++ b/de.gebit.integrity.tests/results/integrity.basic.beans.simpleBeanWithInexistenceTest.xml @@ -0,0 +1,36 @@ + + + + + + + + This should succeed + + + + + + + + + + + This should fail + + + + + + + + + + + + + + + + +