/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * SourceModel_Test.java * Creation date: Mar 2, 2005. * By: Joseph Wong */ package org.openquark.cal.compiler; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import junit.extensions.TestSetup; import junit.framework.Assert; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import org.openquark.cal.runtime.MachineType; import org.openquark.cal.services.BasicCALServices; import org.openquark.cal.services.CALServicesTestUtilities; import org.openquark.cal.services.Status; /** * A set of JUnit test cases for verifying the correctness of the source model. * * @author Joseph Wong */ public class SourceModel_Test extends TestCase { /** * A copy of CAL services for use in the test cases. */ private static BasicCALServices leccCALServices; /** * Set this flag to true if debugging output is desired regardless of * whether a test fails or succeeds. */ private static final boolean SHOW_DEBUGGING_OUTPUT = false; /** * @return a test suite containing all the test cases for testing CAL source * generation. */ public static Test suite() { TestSuite suite = new TestSuite(SourceModel_Test.class); return new TestSetup(suite) { @Override protected void setUp() { oneTimeSetUp(); } @Override protected void tearDown() { oneTimeTearDown(); } }; } /** * Performs the setup for the test suite. */ private static void oneTimeSetUp() { leccCALServices = CALServicesTestUtilities.getCommonCALServices(MachineType.LECC, "cal.platform.test.cws"); } /** * Performs the tear down for the test suite. */ private static void oneTimeTearDown() { leccCALServices = null; ReflectiveFieldsFinder.INSTANCE.cleanup(); } /** * Constructor for SourceModel_Test. * * @param name * the name of the test */ public SourceModel_Test(String name) { super(name); } /** * REGRESSION TEST case for the SourceModel/SourceModelBuilder correctly constructing a model * which contains a representation of a friend declaration. */ public void testSourceModelFriendDecl() { String src = "module Foo; import Prelude; friend FooFriend;"; SourceModel.ModuleDefn sourceModel = SourceModelUtilities.TextParsing.parseModuleDefnIntoSourceModel(src); assertEquals(sourceModel.getNFriendModules(), 1); SourceModel.Friend friend = sourceModel.getNthFriendModule(0); assertEquals(ModuleName.make("FooFriend"), SourceModel.Name.Module.toModuleName(friend.getFriendModuleName())); assertTrue("The source model should have modeled the 'friend FooFriend;' declaration", getSourceText(sourceModel).indexOf("friend FooFriend;") > 0); ParseTreeNode friendParseTreeNode = friend.toParseTreeNode(); assertEquals(friendParseTreeNode.getType(), CALTreeParserTokenTypes.LITERAL_friend); assertEquals(friendParseTreeNode.getNumberOfChildren(), 1); assertEquals(friendParseTreeNode.firstChild().getType(), CALTreeParserTokenTypes.HIERARCHICAL_MODULE_NAME); } public void testSourceModelTraversalWithImportAugmenter() { ModuleSourceDefinitionGroup moduleDefnGroup = CALServicesTestUtilities.getModuleSourceDefinitionGroup(leccCALServices); int nModules = moduleDefnGroup.getNModules(); for (int i = 0; i < nModules; i++) { // For each module source definition, obtain its definition in CAL text form // by reading its contents into a string ModuleSourceDefinition moduleSource = moduleDefnGroup.getModuleSource(i); Status status = new Status("Proccessing module " + moduleSource.getModuleName()); Reader reader = moduleSource.getSourceReader(status); assertTrue("Module " + moduleSource.getModuleName() + " failed to provide a reader", status.isOK()); BufferedReader bufferedReader = new BufferedReader(reader); StringBuilder stringBuf = new StringBuilder(); String line = null; try { while ((line = bufferedReader.readLine()) != null) { stringBuf.append(line).append('\n'); } } catch (IOException e) { fail("Failed to read module " + moduleSource.getModuleName()); } // Step 1: Parse the CAL text for the source of the module // source text -> parse tree ParseTreeNode parseTreeNode = CompilerTestUtilities.parseModuleDefnIntoParseTreeNode(stringBuf.toString()); // If the parse was successful, the parseTreeNode will be non-null and be the root // of the parse tree. Otherwise, the module is broken, and is not suitable for the // purpose of testing the source model. if (parseTreeNode != null) { // Step 2: parse tree -> source model SourceModel.ModuleDefn sourceModel = SourceModelBuilder.buildModuleDefn(parseTreeNode); // Step 3: obtain a new source model augmented with all the required imports SourceModel.ModuleDefn newSourceModel = SourceModelUtilities.ImportAugmenter.augmentWithImports(sourceModel); // Since these modules are expected to compile without problems, they should have // all the imports that are required, and hence the new source model should be identical // to that of the original one. assertEquals(getSourceText(sourceModel), getSourceText(newSourceModel)); } } } /** * A reflection-based test case for verifying that the SourceModelCopier copies every single SourceElement * in a source model, and leaves nothing uncopied. */ public void testSourceModelCopier() { ModuleSourceDefinitionGroup moduleDefnGroup = CALServicesTestUtilities.getModuleSourceDefinitionGroup(leccCALServices); int nModules = moduleDefnGroup.getNModules(); for (int i = 0; i < nModules; i++) { // For each module source definition, obtain its definition in CAL text form // by reading its contents into a string ModuleSourceDefinition moduleSource = moduleDefnGroup.getModuleSource(i); Status status = new Status("Proccessing module " + moduleSource.getModuleName()); Reader reader = moduleSource.getSourceReader(status); assertTrue("Module " + moduleSource.getModuleName() + " failed to provide a reader", status.isOK()); BufferedReader bufferedReader = new BufferedReader(reader); StringBuilder stringBuf = new StringBuilder(); String line = null; try { while ((line = bufferedReader.readLine()) != null) { stringBuf.append(line).append('\n'); } } catch (IOException e) { fail("Failed to read module " + moduleSource.getModuleName()); } // Step 1: Parse the CAL text for the source of the module // source text -> parse tree ParseTreeNode parseTreeNode = CompilerTestUtilities.parseModuleDefnIntoParseTreeNode(stringBuf.toString()); // If the parse was successful, the parseTreeNode will be non-null and be the root // of the parse tree. Otherwise, the module is broken, and is not suitable for the // purpose of testing the source model. if (parseTreeNode != null) { // Step 2: parse tree -> source model SourceModel.ModuleDefn sourceModel = SourceModelBuilder.buildModuleDefn(parseTreeNode); // Step 3: get new source model with copier SourceModel.ModuleDefn newSourceModel = (SourceModel.ModuleDefn)sourceModel.accept(new SourceModelCopier<Void>(), null); // Step 4: compare for structural equality ReflectiveSourceModelStructuralComparer comparer = new ReflectiveSourceModelStructuralComparer(); assertTrue("Structural comparison failed for module " + moduleSource.getModuleName() + " in testSourceModelCopier", comparer.areStructurallyEqual(sourceModel, newSourceModel)); } } } /** * A dynamic test of the source model, getting the source of each CAL module * in the workspace. The source text is first converted to source model * form. Then the source model is copied via the SourceModelCopier. The * resulting source model is converted back to source text, and another * source model is constructed from this generated source text. The * assertion is that these source models are identical to one another. * * We want to test the functionality: * SourceModelBuilder.buildModuleDefn :: ParseTreeNode -> SourceModel * SourceModel.toParseTreeNode :: SourceModel -> ParseTreeNode * SourceModelCopier.visit<source model element> :: SourceModel -> SourceModel * SourceModel.toSourceText :: SourceModel -> String * * on the CAL modules defined in the CAL workspace. * * This is done by taking independent paths to a textual representation and verifying * that they are equal. */ public void testRoundTrip_ModuleDefnsFromWorkspace() { ModuleSourceDefinitionGroup moduleDefnGroup = CALServicesTestUtilities.getModuleSourceDefinitionGroup(leccCALServices); int nModules = moduleDefnGroup.getNModules(); for (int i = 0; i < nModules; i++) { // For each module source definition, obtain its definition in CAL text form // by reading its contents into a string ModuleSourceDefinition moduleSource = moduleDefnGroup.getModuleSource(i); if (SHOW_DEBUGGING_OUTPUT) { System.out.println("Testing module: " + moduleSource.getModuleName().toString()); } Status status = new Status("Proccessing module " + moduleSource.getModuleName()); Reader reader = moduleSource.getSourceReader(status); assertTrue("Module " + moduleSource.getModuleName() + " failed to provide a reader", status.isOK()); BufferedReader bufferedReader = new BufferedReader(reader); StringBuilder stringBuf = new StringBuilder(); String line = null; try { while ((line = bufferedReader.readLine()) != null) { stringBuf.append(line).append('\n'); } } catch (IOException e) { fail("Failed to read module " + moduleSource.getModuleName()); } String origSource = stringBuf.toString(); // Step 1: Parse the CAL text for the source of the module // source text (1) -> parse tree (1) List<SourceEmbellishment> emebellishments = new ArrayList<SourceEmbellishment>(); ParseTreeNode parseTreeNode = CompilerTestUtilities.parseModuleDefnIntoParseTreeNode(stringBuf.toString(), emebellishments); // If the parse was successful, the parseTreeNode will be non-null and be the root // of the parse tree. Otherwise, the module is broken, and is not suitable for the // purpose of testing the source model. if (parseTreeNode != null) { // Step 2: parse tree (1) -> source model (1) SourceModel.ModuleDefn sourceModel = SourceModelBuilder.buildModuleDefn(parseTreeNode); //** CHECK 1: SourceModelBuilder.buildModuleDefn :: ParseTreeNode -> SourceModel // - SourceModelBuilder.buildModuleDefn() should return a non-null value assertNotNull(sourceModel); // Step 2.5: source model (1) -> source text (2) String sourceModelToSourceText = getSourceText(sourceModel); //** CHECK 2: SourceModel.toSourceText :: SourceModel -> String // - SourceModel.toSourceText() should return a non-empty string assertTrue(sourceModelToSourceText.length() > 0); // Step 3: source model (1) -> source model (2) via the SourceModelCopier SourceModel.ModuleDefn sourceModelCopy = (SourceModel.ModuleDefn)sourceModel.accept(new SourceModelCopier<Void>(), null); //** CHECK 3: SourceModelCopier.visit<source model element> :: SourceModel -> SourceModel // - the original source model and the copy should be identical // - the check is done via comparing their generated source texts assertEquals(sourceModelToSourceText, getSourceText(sourceModelCopy)); // Step 4: source model (1) -> parse tree (2) ParseTreeNode newParseTreeNode = sourceModel.toParseTreeNode(); // Step 5: parse tree (2) -> source model (3) SourceModel.ModuleDefn sourceModelFromNewParseTree = SourceModelBuilder.buildModuleDefn(newParseTreeNode); //** CHECK 4: SourceModel.toParseTreeNode :: SourceModel -> ParseTreeNode // - the original source model and the one that is obtained via a round-trip of // SourceModel->ParseTreeNode->SourceModel should be identical // - the check is done via comparing their generated source texts assertEquals(sourceModelToSourceText, getSourceText(sourceModelFromNewParseTree)); // Step 4: source text (2) -> parse tree (3) ParseTreeNode newParseTreeNodeFromGeneratedText = CompilerTestUtilities.parseModuleDefnIntoParseTreeNode(sourceModelToSourceText); // Step 5: parse tree (3) -> source model (4) SourceModel.ModuleDefn sourceModelFromParseTreeFromGeneratedText = SourceModelBuilder.buildModuleDefn(newParseTreeNodeFromGeneratedText); //TODO-MB The following tests have been disabled - there //are some errors in the formatter which breaks its idempotent property, //Also some wrapped words can be 'jiggled' around when the //source model description of CalDoc changes, which //affects the running of these tests. //** CHECK 5: SourceModel.toSourceText :: SourceModel -> String // - SourceModel.toSourceText() should generate parsable text // - the original source model and the one that is obtained via a round-trip of // SourceModel->text->ParseTreeNode->SourceModel should be identical // - the check is done via comparing their generated source texts assertEquals(sourceModelToSourceText, getSourceText(sourceModelFromParseTreeFromGeneratedText)); if (sourceModelToSourceText.compareTo(getSourceText(sourceModelFromParseTreeFromGeneratedText)) != 0) { System.err.println("Broken"); } /// Step 6: check formatting with comments //here we check that the formatted module can be parsed, and that the //toString of the parsed formatted module is correct String sourceModelToSourceTextWithComments = getSourceText(sourceModel, emebellishments); ParseTreeNode newParseTreeNodeFromGeneratedTextWithComments = CompilerTestUtilities.parseModuleDefnIntoParseTreeNode(sourceModelToSourceTextWithComments); SourceModel.ModuleDefn sourceModelFromParseTreeFromGeneratedTextWithComments = SourceModelBuilder.buildModuleDefn(newParseTreeNodeFromGeneratedTextWithComments); assertEquals( getSourceText(sourceModelFromParseTreeFromGeneratedTextWithComments), sourceModelToSourceText); //step 7: check that pretty printing is idempotent //reformat the formatted text and check that is unchanged assertEquals(reformat(sourceModelToSourceTextWithComments), sourceModelToSourceTextWithComments); //step 8: check that the reformatted code has the same number of parens //as the original source. if (countParen(sourceModelToSourceTextWithComments) != countParen(origSource)) { int str1 = 1; str1++; } assertEquals(countParen(sourceModelToSourceTextWithComments),countParen(origSource)); if (SHOW_DEBUGGING_OUTPUT) { System.out.println("////// Source of " + moduleSource.getModuleName()); System.out.println(sourceModelToSourceText); } } } } /** * count the number of opening paren in a string. * @param s1 * @return the number of opening paren */ private int countParen(String s1) { int i=0, numParen=0; for(i=0; i<s1.length(); i++) { if (s1.charAt(i) == '(') { numParen++; } } return numParen; } /** * this helper reformats a source module string, including comments * @param input * @return formatted source */ private String reformat(String input) { List<SourceEmbellishment> emebellishments = new ArrayList<SourceEmbellishment>(); ParseTreeNode parseTreeNode = CompilerTestUtilities.parseModuleDefnIntoParseTreeNode(input, emebellishments); // Step 5: parse tree (2) -> source model (3) SourceModel.ModuleDefn sourceModel = SourceModelBuilder.buildModuleDefn(parseTreeNode); return getSourceText(sourceModel, emebellishments); } /** * Tests that each concrete subclass of SourceElement has a public static make* factory method. * The classes which are exempt from this check are listed in the variable <code>exemptClasses</code>. */ public void testPublicFactoryMethods() { final Set<Class<?>> concreteSourceElementSubclasses = getConcreteSourceElementSubclasses(SourceModel.class); final Class<?>[] exemptClasses = new Class<?>[] { SourceModel.FunctionDefn.Primitive.class }; final List<Class<?>> classesWithoutPublicFactoryMethods = new ArrayList<Class<?>>(); for (final Class<?> concreteSubclass : concreteSourceElementSubclasses) { final Method[] methods = concreteSubclass.getMethods(); boolean foundPublicStaticMake = false; for (final Method method : methods) { final int modifiers = method.getModifiers(); if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers)) { if (method.getName().startsWith("make")) { foundPublicStaticMake = true; break; } } } if (!foundPublicStaticMake) { classesWithoutPublicFactoryMethods.add(concreteSubclass); } } classesWithoutPublicFactoryMethods.removeAll(Arrays.asList(exemptClasses)); assertEquals(Collections.EMPTY_LIST, classesWithoutPublicFactoryMethods); } /** * Retrieves all the concrete subclasses of SourceElement that appear as inner classes of the given class. * @param rootClass the root of the containment hierarchy to search. * @return a Set of Class objects. */ private Set<Class<?>> getConcreteSourceElementSubclasses(final Class<?> rootClass) { final Set<Class<?>> result = new HashSet<Class<?>>(); final Class<?>[] innerClasses = rootClass.getDeclaredClasses(); for (final Class<?> innerClass : innerClasses) { final int modifiers = innerClass.getModifiers(); if (Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers)) { if (SourceModel.SourceElement.class.isAssignableFrom(innerClass)) { result.add(innerClass); } } result.addAll(getConcreteSourceElementSubclasses(innerClass)); } return result; } /** * Converts a source model element into source text without embellishments * @param sourceElement * @return the corresponding source text. */ private static String getSourceText(final SourceModel.SourceElement sourceElement) { return SourceModelCodeFormatter.formatCode(sourceElement, SourceModelCodeFormatter.DEFAULT_OPTIONS, Collections.EMPTY_LIST); } /** * Converts a source model element into source text with embellishments * @param sourceElement * @return the corresponding source text. */ private static String getSourceText(final SourceModel.SourceElement sourceElement, List<SourceEmbellishment> embellishments) { return SourceModelCodeFormatter.formatCode(sourceElement, SourceModelCodeFormatter.DEFAULT_OPTIONS, embellishments); } /** * Asserts that two strings are equal, and print out both strings in their entirety * when they are not equal. */ public static void assertEquals(String a, String b) { Assert.assertEquals("Strings differ\narg1:\n" + a + "\narg2:\n" + b, a, b); } /** * A utility class for finding all non static fields (even protected and private ones) * in a class and all its superclasses. * * @author Joseph Wong */ static final class ReflectiveFieldsFinder { /** Memoized results from previous runs. */ private final Map<Class<?>, Field[]> classToFields = new HashMap<Class<?>, Field[]>(); /** * A map mapping a Field to its original accessibility flag, for use * during cleanup when the accessibility flags should be reset back * to their original values. */ private final Map<Field, Boolean> fieldAccessibility = new HashMap<Field, Boolean>(); /** The singleton instance. */ static final ReflectiveFieldsFinder INSTANCE = new ReflectiveFieldsFinder(); /** Private constructor. */ private ReflectiveFieldsFinder() {} /** * Gets an array of the non-static fields of the given class and all its superclasses. * @param cls the Class to query. * @return all the non-static fields (even private and protected ones) of the class and its superclasses. */ Field[] getAllNonStaticFields(Class<?> cls) { Field[] fields = classToFields.get(cls); if (fields != null) { return fields; } List<Field> fieldList = new ArrayList<Field>(); Class<?> currentClass = cls; while (currentClass != null) { Field[] curClassFields = currentClass.getDeclaredFields(); for (final Field f : curClassFields) { if (!Modifier.isStatic(f.getModifiers())) { fieldAccessibility.put(f, Boolean.valueOf(f.isAccessible())); f.setAccessible(true); fieldList.add(f); } } currentClass = currentClass.getSuperclass(); } fields = fieldList.toArray(new Field[0]); classToFields.put(cls, fields); return fields; } /** * Cleans up by reseting the accessibility flags modified during invocations to getAllNonStaticFields(). */ void cleanup() { for (final Map.Entry<Field, Boolean> entry : fieldAccessibility.entrySet()) { Field field = entry.getKey(); Boolean isAccessible = entry.getValue(); field.setAccessible(isAccessible.booleanValue()); } classToFields.clear(); fieldAccessibility.clear(); } @Override public void finalize() { cleanup(); } } /** * A utility for comparing two SourceElements using structural equivalence. * * @author Joseph Wong */ static final class ReflectiveSourceModelStructuralComparer { /** * @return true if the two SourceElements are structually equal. */ boolean areStructurallyEqual(SourceModel.SourceElement a, SourceModel.SourceElement b) { if (a == null || b == null) { return a == null && b == null; } Class<? extends SourceModel.SourceElement> aClass = a.getClass(); Class<? extends SourceModel.SourceElement> bClass = b.getClass(); if (!aClass.equals(bClass)) { fail("Not equal:\n" + a + "\nand:\n" + b); return false; } Field[] fields = ReflectiveFieldsFinder.INSTANCE.getAllNonStaticFields(aClass); for (final Field field : fields) { try { Object aFieldValue = field.get(a); Object bFieldValue = field.get(b); if (!areFieldValuesStructurallyEqual(aFieldValue, bFieldValue)) { fail("Not equal:\n" + aFieldValue + "\nand:\n" + bFieldValue); return false; } } catch (Exception e) { fail("Exception thrown: " + e); } } return true; } /** * @return true if the two Objects are structually equal, in that if they are SourceElements, then * they are compared via areStructurallyEqual(). Otherwise, regular equals() is employed. (For classes * which overrides toString() but not equals(), the string representations are compared as a last resort). */ <T> boolean areFieldValuesStructurallyEqual(T a, T b) { // handle null case if (a == null || b == null) { if (a == null && b == null) { return true; } else { final Class<?> cls = (a == null) ? b.getClass() : a.getClass(); fail("Not equal (" + cls + "):\n" + a + "\nand:\n" + b); return false; } } // handle case when both are SourceElements if (a instanceof SourceModel.SourceElement && b instanceof SourceModel.SourceElement) { return areStructurallyEqual((SourceModel.SourceElement)a, (SourceModel.SourceElement)b); } // handle case when both are arrays of objects if (a instanceof Object[] && b instanceof Object[]) { Object[] aArray = (Object[])a; Object[] bArray = (Object[])b; if (aArray.length != bArray.length) { fail("Not equal:\n" + Arrays.asList(aArray) + "\nand:\n" + Arrays.asList(bArray)); return false; } for (int j = 0; j < aArray.length; j++) { if (!areFieldValuesStructurallyEqual(aArray[j], bArray[j])) { fail("Not equal:\n" + aArray[j] + "\nand:\n" + bArray[j]); return false; } } return true; } // a and/or b may be a primitive, a primitive array, or an object outside the SourceElement hierarchy boolean areEqual = false; if (a instanceof boolean[] && b instanceof boolean[]) { areEqual = Arrays.equals((boolean[])a, (boolean[])b); } else if (a instanceof byte[] && b instanceof byte[]) { areEqual = Arrays.equals((byte[])a, (byte[])b); } else if (a instanceof char[] && b instanceof char[]) { areEqual = Arrays.equals((char[])a, (char[])b); } else if (a instanceof double[] && b instanceof double[]) { areEqual = Arrays.equals((double[])a, (double[])b); } else if (a instanceof float[] && b instanceof float[]) { areEqual = Arrays.equals((float[])a, (float[])b); } else if (a instanceof int[] && b instanceof int[]) { areEqual = Arrays.equals((int[])a, (int[])b); } else if (a instanceof long[] && b instanceof long[]) { areEqual = Arrays.equals((long[])a, (long[])b); } else if (a instanceof short[] && b instanceof short[]) { areEqual = Arrays.equals((short[])a, (short[])b); } else { // default to standard equals(), or string comparison if equals() still refers to Object.equals() areEqual = a.equals(b) || a.toString().equals(b.toString()); } if (!areEqual) { fail("Not equal:\n" + a + "\nand:\n" + b); } return areEqual; } } }