/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.test_utils; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Scanner; import java.util.Set; import org.junit.Assert; import org.junit.ComparisonFailure; import com.rapidminer.example.Attribute; import com.rapidminer.example.AttributeRole; import com.rapidminer.example.Attributes; import com.rapidminer.example.Example; import com.rapidminer.example.table.NominalMapping; import com.rapidminer.operator.Annotations; import com.rapidminer.operator.IOObject; import com.rapidminer.operator.meta.ParameterValue; import com.rapidminer.tools.Ontology; import junit.framework.AssertionFailedError; /** * Extension for JUnit's Assert for testing RapidMiner objects. * * @author Simon Fischer, Marcin Skirzynski, Marius Helf * */ public class RapidAssert extends Assert { public static final double DELTA = 0.000000001; public static final double MAX_RELATIVE_ERROR = 0.000000001; public static final AsserterRegistry ASSERTER_REGISTRY = new AsserterRegistry(); private static boolean ignoreRepositoryNameForSourceAnnotation = true; /** * Returns <code>true</code> if the ioobjects class is supported for comparison in the test * extension and <code>false</code> otherwise. */ public static boolean comparable(IOObject ioobject) { return ASSERTER_REGISTRY.getAsserterForObject(ioobject) != null; } /** * Returns <code>true</code> if both ioobject classes are comparable to each other and * <code>false</code> otherwise. */ public static boolean comparable(IOObject ioobject1, IOObject ioobject2) { return ASSERTER_REGISTRY.getAsserterForObjects(ioobject1, ioobject2) != null; } /** * Extends the Junit assertEquals method by additionally checking the doubles for NaN. * * @param message * message to display if an error occurs * @param expected * expected value * @param actual * actual value */ public static void assertEqualsNaN(String message, double expected, double actual) { if (Double.isNaN(expected)) { if (!Double.isNaN(actual)) { throw new AssertionFailedError(message + " expected: <" + expected + "> but was: <" + actual + ">"); } } else { assertEquals(message, expected, actual, DELTA); } } /** * Attention: Does not work with values near 0!! */ public static void assertEqualsWithRelativeErrorOrBothNaN(String message, double expected, double actual) { if (expected == actual) { return; } if (Double.isNaN(expected) && !Double.isNaN(actual)) { throw new AssertionFailedError(message + " expected: <" + expected + "> but was: <" + actual + ">"); } if (!Double.isNaN(expected) && Double.isNaN(actual)) { throw new AssertionFailedError(message + " expected: <" + expected + "> but was: <" + actual + ">"); } double relativeError; if (Math.abs(actual) > Math.abs(expected)) { relativeError = Math.abs((expected - actual) / actual); } else { relativeError = Math.abs((expected - actual) / expected); } if (relativeError > MAX_RELATIVE_ERROR) { throw new AssertionFailedError(message + " expected: <" + expected + "> but was: <" + actual + ">"); } } /** * Tests if the special names of the attribute roles are equal and the associated attributes * themselves. * * @param message * message to display if an error occurs * @param expected * expected value * @param actual * actual value * @param compareDefaultValues */ public static void assertEquals(String message, AttributeRole expected, AttributeRole actual, boolean compareDefaultValues) { Assert.assertEquals(message + " (attribute role)", expected.getSpecialName(), actual.getSpecialName()); Attribute expectedAttribute = expected.getAttribute(); Attribute actualAttribute = actual.getAttribute(); assertEquals(message, expectedAttribute, actualAttribute, compareDefaultValues); } /** * Tests two attributes by using the name, type, block, type, default value and the nominal * mapping * * @param message * message to display if an error occurs * @param expected * expected value * @param actual * actual value */ public static void assertEquals(String message, Attribute expected, Attribute actual, boolean compareDefaultValues) { Assert.assertEquals(message + " (attribute name)", expected.getName(), actual.getName()); Assert.assertEquals(message + " (attribute type of attribute '" + expected.getName() + "': expected '" + Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(expected.getValueType()) + "' but was '" + Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(actual.getValueType()) + "')", expected.getValueType(), actual.getValueType()); Assert.assertEquals(message + " (attribute block type of attribute '" + expected.getName() + ": expected '" + Ontology.ATTRIBUTE_BLOCK_TYPE.mapIndex(expected.getBlockType()) + "' but was '" + Ontology.ATTRIBUTE_BLOCK_TYPE.mapIndex(actual.getBlockType()) + "')", expected.getBlockType(), actual.getBlockType()); if (compareDefaultValues) { assertEquals(message + " (default value of attribute '" + expected.getName() + ")", expected.getDefault(), actual.getDefault(), DELTA); } if (expected.isNominal()) { assertEqualsIgnoreOrder(message + " (nominal mapping of attribute '" + expected.getName() + ")", expected.getMapping(), actual.getMapping()); } } /** * Tests two nominal mappings for its size and values. * * @param message * message to display if an error occurs * @param expected * expected value * @param actual * actual value * @param ignoreOrder * if <code>true</code> the order of the mappings is not checked, but only the size * of the mapping and that all values of <code>expected</code> are present in * <code>actual</code>. */ public static void assertEquals(String message, NominalMapping expected, NominalMapping actual, boolean ignoreOrder) { if (expected == actual) { return; } Assert.assertTrue(expected == null && actual == null || expected != null && actual != null); if (expected == null || actual == null) { return; } Assert.assertEquals(message + " (nominal mapping size)", expected.size(), actual.size()); List<String> expectedValues = expected.getValues(); List<String> actualValues = actual.getValues(); // check that we have the same values in both mappings: Set<String> expectedValuesSet = new HashSet<String>(expectedValues); Set<String> actualValuesSet = new HashSet<String>(actualValues); Assert.assertEquals(message + " (different nominal values)", expectedValuesSet, actualValuesSet); if (!ignoreOrder) { // check order Iterator<String> expectedIt = expectedValues.iterator(); while (expectedIt.hasNext()) { String expectedValue = expectedIt.next(); Assert.assertEquals(message + " (index of nominal value '" + expectedValue + "')", expected.mapString(expectedValue), actual.mapString(expectedValue)); } } } /** * Tests two nominal mappings for its size and values. * * @param message * message to display if an error occurs * @param expected * expected value * @param actual * actual value */ public static void assertEqualsIgnoreOrder(String message, NominalMapping expected, NominalMapping actual) { assertEquals(message, expected, actual, true); } /** * Tests all objects in the array. * * @param expected * array with expected objects * @param actual * array with actual objects */ public static void assertArrayEquals(String message, Object[] expected, Object[] actual) { if (expected == null) { assertEquals((Object) null, actual); return; } if (actual == null) { throw new AssertionFailedError(message + " (expected " + Arrays.toString(expected) + " , but is null)"); } assertEquals(message + " (array length is not equal)", expected.length, actual.length); for (int i = 0; i < expected.length; i++) { assertEquals(message, expected[i], actual[i]); } } public static void assertArrayEquals(String message, byte[] expected, byte[] actual) { if (expected == null) { assertEquals((Object) null, actual); return; } if (actual == null) { throw new AssertionFailedError(message + " (expected " + Arrays.toString(expected) + " , but is null)"); } assertEquals(message + " (array length is not equal)", expected.length, actual.length); for (int i = 0; i < expected.length; i++) { assertEquals(message, expected[i], actual[i]); } } /** * Compares a string linewise, i.e. ignores different linebreak characters. * * Does this by incrementally reading all expected an actual lines and comparing them linewise. * * @param message * @param expected * @param actual */ public static void assertLinewiseEquals(String message, String expected, String actual) { try (Scanner expectedScanner = new Scanner(expected); Scanner actualScanner = new Scanner(actual);) { String expectedLine = null; String actualLine = null; int lineCounter = 1; while (expectedScanner.hasNextLine()) { expectedLine = expectedScanner.nextLine(); if (actualScanner.hasNextLine()) { actualLine = actualScanner.nextLine(); } else { fail("Line " + lineCounter + ": actual input has less lines then expected result! Expected: " + expectedLine); } assertEquals("Line " + lineCounter + ": " + message + "\n\nExpected:\n" + expected + "\nActual:\n" + actual, expectedLine, actualLine); ++lineCounter; } } } /** * Tests all objects in the array. * * @param message * message to display if an error occurs * @param expected * array with expected objects * @param actual * array with actual objects */ public static void assertArrayEquals(Object[] expected, Object[] actual) { assertArrayEquals("", expected, actual); } /** * Tests if both list of ioobjects are equal. * * @param expected * expected value * @param actual * actual value */ public static void assertEquals(String message, List<IOObject> expected, List<IOObject> actual) { assertSize(expected, actual); Iterator<IOObject> expectedIter = expected.iterator(); Iterator<IOObject> actualIter = actual.iterator(); int objectIndex = 1; while (expectedIter.hasNext() && actualIter.hasNext()) { IOObject expectedIOO = expectedIter.next(); IOObject actualIOO = actualIter.next(); String subMessage = message + " IOObject \"" + actualIOO.getSource() + "\" at position " + objectIndex + " does not match the expected value "; assertEquals(subMessage, expectedIOO, actualIOO); objectIndex++; } } /** * Tests if both list of ioobjects are equal. * * @param expected * expected value * @param actual * actual value */ public static void assertEquals(List<IOObject> expected, List<IOObject> actual) { RapidAssert.assertEquals("", expected, actual); } /** * Tests if both lists of IOObjects have the same size. * * @param expected * @param actual */ public static void assertSize(List<IOObject> expected, List<IOObject> actual) { assertEquals( "Number of connected output ports in the process is not equal with the number of ioobjects contained in the same folder with the format 'processname-expected-port-1', 'processname-expected-port-2', ...", expected.size(), actual.size()); } /** * Tests if the two IOObjects are equal. * * @param expectedIOO * @param actualIOO */ public static void assertEquals(IOObject expectedIOO, IOObject actualIOO) { RapidAssert.assertEquals("", expectedIOO, actualIOO); } /** * Tests if the two IOObjects are equal and appends the given message. * * @param expectedIOO * @param actualIOO */ public static void assertEquals(String message, IOObject expectedIOO, IOObject actualIOO) { assertEquals(message, expectedIOO, actualIOO, false); } /** * Tests if the two IOObjects are equal and appends the given message. * * @param assertEqualAnnotations * if true, annotations will be compared. If false, they will be ignored. * @param expectedIOO * @param actualIOO */ public static void assertEquals(String message, IOObject expectedIOO, IOObject actualIOO, boolean assertEqualAnnotations) { /* * Do not forget to add a newly supported class to the ASSERTER_REGISTRY!!! */ List<Asserter> asserterList = ASSERTER_REGISTRY.getAsserterForObjects(expectedIOO, actualIOO); if (asserterList != null) { for (Asserter asserter : asserterList) { asserter.assertEquals(message, expectedIOO, actualIOO); } } else { throw new ComparisonFailure("Comparison of the two given IOObject classes " + expectedIOO.getClass() + " and " + actualIOO.getClass() + " is not supported. ", expectedIOO.toString(), actualIOO.toString()); } // last, compare annotations: if (assertEqualAnnotations) { Annotations expectedAnnotations = expectedIOO.getAnnotations(); Annotations actualAnnotations = actualIOO.getAnnotations(); if (ignoreRepositoryNameForSourceAnnotation) { // compare annotations one by one. For the Source annotation, ignore the repository // name // (that's what all the regular expressions here are for) for (String key : expectedAnnotations.getKeys()) { String expectedValue = expectedAnnotations.getAnnotation(key); String actualValue = actualAnnotations.getAnnotation(key); if (expectedValue != null) { Assert.assertNotNull(message + "objects are equal, but annotation '" + key + "' is missing", actualValue); } if (Annotations.KEY_SOURCE.equals(key)) { if (expectedValue != null && expectedValue.startsWith("//") && expectedValue.matches("//[^/]+/.*")) { expectedValue = expectedValue.replaceAll("^//[^/]+/", "//repository/"); if (actualValue != null) { actualValue = actualValue.replaceAll("^//[^/]+/", "//repository/"); } } } Assert.assertEquals(message + "objects are equal, but annotation '" + key + "' differs: ", expectedValue, actualValue); } } else { Assert.assertEquals(message + "objects are equal, but annotations differ: ", expectedAnnotations, actualAnnotations); } } } /** * Tests the two examples by testing the value of the examples for every given attribute. This * method is sensitive to the regular attribute ordering. * * @param message * message to display if an error occurs. If it contains "{0}" and "{1}", it will be * replaced with the attribute name and attribute type, if an inequality occurs. * @param expected * expected value * @param actual * actual value */ public static void assertEquals(String message, Example expected, Example actual) { Assert.assertEquals(message + " (number of attributes)", expected.getAttributes().allSize(), actual.getAttributes() .allSize()); Assert.assertEquals(message + " (number of special attributes)", expected.getAttributes().specialSize(), actual .getAttributes().specialSize()); // get all attributes as list Iterator<Attribute> allExpectedAttributesIterator = expected.getAttributes().allAttributes(); Iterator<Attribute> allActualAttributesIterator = actual.getAttributes().allAttributes(); List<Attribute> allExpectedAttributes = new ArrayList<Attribute>(); while (allExpectedAttributesIterator.hasNext()) { allExpectedAttributes.add(allExpectedAttributesIterator.next()); } List<Attribute> allActualAttributes = new ArrayList<Attribute>(); while (allActualAttributesIterator.hasNext()) { allActualAttributes.add(allActualAttributesIterator.next()); } // get regular attributes as iterator Iterator<Attribute> expectedAttributesToConsider = expected.getAttributes().iterator(); Iterator<Attribute> actualAttributesToConsider = actual.getAttributes().iterator(); // first check regular attributes sensitive to the attribute ordering while (expectedAttributesToConsider.hasNext() && actualAttributesToConsider.hasNext()) { Attribute a1 = expectedAttributesToConsider.next(); Attribute a2 = actualAttributesToConsider.next(); if (!a1.getName().equals(a2.getName())) { // this should have been detected by previous checks already throw new AssertionFailedError("Attribute ordering does not match: " + a1.getName() + "," + a2.getName()); } if (a1.isNominal()) { Assert.assertEquals(MessageFormat.format(message, "nominal", a1.getName()), expected.getNominalValue(a1), actual.getNominalValue(a2)); } else if (a1.isNumerical()) { assertEqualsWithRelativeErrorOrBothNaN(MessageFormat.format(message, "numerical", a1.getName()), expected.getValue(a1), actual.getValue(a2)); } else { Assert.assertEquals(expected.getDateValue(a1), actual.getDateValue(a2)); } // passed, so delete regular attribute from all attributes list. allExpectedAttributes.remove(a1); allActualAttributes.remove(a2); } // check the remaining special attributes for (int i = 0; i < allExpectedAttributes.size(); i++) { Attribute expectedSpecial = allExpectedAttributes.get(i); String expectedName = expectedSpecial.getName(); String actualName = null; for (int j = 0; i < allActualAttributes.size(); j++) { Attribute actualSpecial = allActualAttributes.get(j); actualName = actualSpecial.getName(); if (expectedName.equals(actualName)) { if (expectedSpecial.isNominal()) { Assert.assertEquals(MessageFormat.format(message, "nominal", expectedSpecial.getName()), expected.getNominalValue(expectedSpecial), actual.getNominalValue(actualSpecial)); } else if (expectedSpecial.isNumerical()) { assertEqualsWithRelativeErrorOrBothNaN( MessageFormat.format(message, "numerical", expectedSpecial.getName()), expected.getValue(expectedSpecial), actual.getValue(actualSpecial)); } else { Assert.assertEquals(expected.getDateValue(expectedSpecial), actual.getDateValue(actualSpecial)); } // remove from list allExpectedAttributes.remove(expectedSpecial); allActualAttributes.remove(actualSpecial); i--; j--; break; } } if (!expectedName.equals(actualName)) { throw new AssertionFailedError("Expected attribute not found: " + expectedSpecial.getName()); } } } /** * Tests if all attributes are equal. This method is sensitive to the regular attribute * ordering. * * Optionally compares the default values of the attributes. The default value is only relevant * for sparse data rows, so it should not be compared for non-sparse data. * * @param message * message to display if an error occurs * @param expected * expected value * @param actual * actual value * @param compareDefaultValues * specifies if the attributes default values should be compared. */ public static void assertEquals(String message, Attributes expected, Attributes actual, boolean compareDefaultValues) { Assert.assertEquals(message + " (number of attributes)", expected.allSize(), actual.allSize()); Assert.assertEquals(message + " (number of special attributes)", expected.specialSize(), actual.specialSize()); Iterator<AttributeRole> expectedRoleIt = expected.regularAttributes(); Iterator<AttributeRole> actualRoleIt = actual.regularAttributes(); while (expectedRoleIt.hasNext()) { AttributeRole expectedRole = expectedRoleIt.next(); AttributeRole actualRole = actualRoleIt.next(); RapidAssert.assertEquals(message, expectedRole, actualRole, compareDefaultValues); } expectedRoleIt = expected.specialAttributes(); while (expectedRoleIt.hasNext()) { AttributeRole expectedRole = expectedRoleIt.next(); AttributeRole actualRole = actual.getRole(actual.getSpecial(expectedRole.getSpecialName())); RapidAssert.assertEquals(message, expectedRole, actualRole, compareDefaultValues); } } public static void assertEquals(String message, ParameterValue expected, ParameterValue actual) { Assert.assertEquals(message + " - operator", expected.getOperator(), actual.getOperator()); Assert.assertEquals(message + " - parameterKey", expected.getParameterKey(), actual.getParameterKey()); Assert.assertEquals(message + " - parameterValue", expected.getParameterValue(), actual.getParameterValue()); } }