001/*
002 * $Id: ObjectTester.java,v 1.55 2017/11/09 20:34:50 oboehm Exp $
003 *
004 * Copyright (c) 2010 by Oliver Boehm
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express orimplied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 *
018 * (c)reated 21.07.2010 by oliver (ob@oasd.de)
019 */
020
021package patterntesting.runtime.junit;
022
023import clazzfish.monitor.ClasspathMonitor;
024import org.apache.logging.log4j.LogManager;
025import org.apache.logging.log4j.Logger;
026import org.junit.jupiter.api.Assertions;
027import patterntesting.runtime.exception.DetailedAssertionError;
028import patterntesting.runtime.util.Converter;
029import patterntesting.runtime.util.ReflectionHelper;
030
031import java.io.NotSerializableException;
032import java.io.Serializable;
033import java.lang.reflect.Field;
034import java.lang.reflect.Method;
035import java.lang.reflect.Modifier;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.Collection;
039import java.util.List;
040import java.util.regex.Pattern;
041
042import static org.junit.jupiter.api.Assertions.assertFalse;
043
044/**
045 * This is a utility class to check some important methods of a class like the
046 * {@link Object#equals(Object)} or {@link Object#hashCode()} method. Before
047 * v1.1 the methods are named "checkEquals" or "checkCompareTo". Since 1.1 these
048 * methods have now an "assert" prefix ("assertEquals" or "assertCompareTo").
049 *
050 * @author oliver
051 * @since 1.0.3 (21.07.2010)
052 */
053public final class ObjectTester extends AbstractTester {
054
055        private static final Logger LOG = LogManager.getLogger(ObjectTester.class);
056        private static final ClasspathMonitor classpathMonitor = ClasspathMonitor.getInstance();
057
058        /** Utility class - no need to instantiate it. */
059        private ObjectTester() {
060        }
061
062        /**
063         * Check equality of the given objects. They must be equals otherwise an
064         * AssertionError will be thrown. And if <tt>A == B</tt> also
065         * <tt>B == A</tt> must be true (commutative law, i.e. it is a symmetrical
066         * operation).
067         * <p>
068         * If two objects are equals they must have also the same hash code (but not
069         * the other way around). This condition is also checked here.
070         * </p>
071         * <p>
072         * Often programmers forget that the {@link Object#equals(Object)} method
073         * can be called with <em>null</em> as argument and should return
074         * <em>false</em> as result. So this case is also tested here.
075         * </p>
076         *
077         * @param o1
078         *            the 1st object
079         * @param o2
080         *            the 2nd object
081         * @throws AssertionError
082         *             if the check fails
083         * @since 1.1
084         */
085        @SuppressWarnings("unchecked")
086        public static void assertEquals(final Object o1, final Object o2) throws AssertionError {
087                if (o1 instanceof Package) {
088                        Package pkg = (Package) o1;
089                        if (o2 instanceof Class) {
090                                Class<?> c2 = (Class<?>) o2;
091                                Class<?>[] excluded = { c2 };
092                                assertEquals(pkg, excluded);
093                        } else if (o2 instanceof Pattern) {
094                                assertAllOfPackage(pkg.getName(), (Pattern) o2);
095                        }
096                } else {
097                        Assertions.assertEquals(o1, o2, o1.getClass() + ": objects are not equals!");
098                        Assertions.assertEquals(o2, o1, o1.getClass() + ": equals not symmetrical (A == B, but B != A)");
099                        Assertions.assertEquals(o1.hashCode(), o2.hashCode(),
100                                        o1.getClass() + ": objects are equals but hashCode differs!");
101                        if (o1 instanceof Comparable<?>) {
102                                ComparableTester.assertCompareTo((Comparable<Comparable<?>>) o1, (Comparable<Comparable<?>>) o1);
103                        }
104                        assertEqualsWithNull(o1);
105                }
106                LOG.info("equals/hashCode implementation of " + o1.getClass() + " seems to be ok");
107        }
108
109        /**
110         * Checks if the two given objects are really not equals. The following
111         * conditions should be fullfilled: if (a != b) then also (b != a) should be
112         * true. This is tested here.
113         *
114         * @param a
115         *            the a
116         * @param b
117         *            the b
118         * @throws AssertionError
119         *             the assertion error
120         * @since 1.5
121         */
122        public static void assertNotEquals(final Object a, final Object b) throws AssertionError {
123                assertFalse(a.equals(b), "expected: '" + a + "' != '" + b + "'");
124                assertFalse(b.equals(a),
125                                a.getClass() + ": equals not symmetrical (A != B, but B == A) with A = '" + a + "' and B = '" + b + "'");
126        }
127
128        /**
129         * Null as argument for the equals method should always return 'false' and
130         * should not end with a NullPointerException.
131         *
132         * @param obj
133         *            the obj
134         */
135        private static void assertEqualsWithNull(final Object obj) {
136                try {
137                        assertFalse(obj.equals(null), obj.getClass().getName() + ".equals(null) should return 'false'");
138                } catch (RuntimeException re) {
139                        throw new DetailedAssertionError(
140                                        obj.getClass().getName() + ".equals(..) implementation does not check (correct) for null argument",
141                                        re);
142                }
143        }
144
145        /**
146         * The given object will be serialized and deserialized to get a copy of
147         * that object. The copy must be equals to the original object.
148         * <p>
149         * If the check fails (e.g. if copy and original object are not equals) an
150         * {@link AssertionError} will be thrown.
151         * </p>
152         *
153         * @param obj
154         *            the object
155         * @throws NotSerializableException
156         *             if obj is not serializable
157         * @since 1.1
158         */
159        public static void assertEquals(final Serializable obj) throws NotSerializableException {
160                Object clone = clone(obj);
161                assertEquals(obj, clone);
162        }
163
164        /**
165         * The given object will be cloned to get a copy of that object. The copy
166         * must be equals to the original object.
167         *
168         * @param obj
169         *            the obj
170         * @throws AssertionError
171         *             the assertion error
172         * @since 1.1
173         */
174        public static void assertEquals(final Cloneable obj) throws AssertionError {
175                Object clone = CloneableTester.getCloneOf(obj);
176                assertEquals(obj, clone);
177        }
178
179        /**
180         * This method will create two objects of the given class using the default
181         * constructor. So three preconditions must be true:
182         * <ol>
183         * <li>the class must not be abstract</li>
184         * <li>there must be a (public) default constructor</li>
185         * <li>it must be Cloneable, Serializable or return always the same object
186         * </li>
187         * </ol>
188         * That a constructor creates equals objects is not true for all classes.
189         * For example the default constructor of the Date class will generate
190         * objects with different timestamps which are not equal. But most classes
191         * should meet the precondition.
192         *
193         * @param clazz
194         *            the clazz
195         * @throws AssertionError
196         *             if the check fails
197         * @since 1.1
198         */
199        public static void assertEquals(final Class<?> clazz) throws AssertionError {
200                LOG.trace("checking {}.equals...", clazz);
201                Object o1 = newInstanceOf(clazz);
202                if (o1 instanceof Cloneable) {
203                        assertEquals((Cloneable) o1);
204                } else if (o1 instanceof Serializable) {
205                        try {
206                                assertEquals((Serializable) o1);
207                        } catch (NotSerializableException nse) {
208                                throw new AssertionError(nse);
209                        }
210                } else {
211                        Object o2 = newInstanceOf(clazz);
212                        assertEquals(o1, o2);
213                }
214        }
215
216        /**
217         * Check for each class in the given collection if the equals() method is
218         * implemented correct.
219         *
220         * @param <T> the generic type
221         * @param classes the classes
222         * @throws Failures the collected assertion errors
223         * @since 1.1
224         */
225        public static <T> void assertEquals(final Collection<Class<? extends T>> classes) throws Failures {
226                Failures failures = new Failures();
227                for (Class<?> clazz : classes) {
228                        try {
229                                assertEquals(clazz);
230                        } catch (AssertionError e) {
231                                LOG.warn("equals/hashCode implementation of " + clazz + " is NOT OK (" + e.getMessage() + ")");
232                                failures.add(clazz, e);
233                        }
234                }
235                if (failures.hasErrors()) {
236                        throw failures;
237                }
238        }
239
240        /**
241         * Check for each class in the given package if the equals() method is
242         * implemented correct.
243         * <p>
244         * To get a name of a package call {@link Package#getPackage(String)}. But
245         * be sure that you can't get <em>null</em> as result. In this case use
246         * {@link #assertEqualsOfPackage(String)}.
247         * </p>
248         *
249         * @param pkg
250         *            the package e.g. "patterntesting.runtime"
251         * @see #assertEqualsOfPackage(String)
252         * @since 1.1
253         */
254        public static void assertEquals(final Package pkg) {
255                assert pkg != null;
256                assertEqualsOfPackage(pkg.getName());
257        }
258
259        /**
260         * Check for each class in the given package if the equals() method is
261         * implemented correct.
262         * <p>
263         * To get a name of a package call {@link Package#getPackage(String)}. But
264         * be sure that you can't get <em>null</em> as result. In this case use
265         * {@link #assertEqualsOfPackage(String, Class...)}.
266         * </p>
267         *
268         * @param pkg
269         *            the package e.g. "patterntesting.runtime"
270         * @param excluded
271         *            classes which are excluded from the check
272         * @see #assertEqualsOfPackage(String, Class...)
273         * @since 1.1
274         */
275        public static void assertEquals(final Package pkg, final Class<?>... excluded) {
276                assert pkg != null;
277                assertEqualsOfPackage(pkg.getName(), excluded);
278        }
279
280        /**
281         * Check for each class in the given package if the equals() method is
282         * implemented correct. E.g. if your unit test classes ends all with
283         * "...Test" and you want to remove them from the check you can call
284         *
285         * <pre>
286         * ObjectTester.assertEquals(pkg, Pattern.compile(".*Test"));
287         * </pre>
288         *
289         * @param pkg
290         *            the package
291         * @param excluded
292         *            class pattern which should be excluded from the check
293         * @since 1.6
294         */
295        public static void assertEquals(final Package pkg, final Pattern... excluded) {
296                assertEqualsOfPackage(pkg.getName(), excluded);
297        }
298
299        /**
300         * Check for each class in the given package if the equals() method is
301         * implemented correct.
302         * <p>
303         * This method does the same as {@link #assertEquals(Package)} but was
304         * introduced by {@link Package#getPackage(String)} sometimes return null if
305         * no class of this package is loaded.
306         * </p>
307         *
308         * @param packageName
309         *            the package name e.g. "patterntesting.runtime"
310         * @see #assertEquals(Package)
311         * @since 1.1
312         */
313        public static void assertEqualsOfPackage(final String packageName) {
314                Collection<Class<? extends Object>> classes = getClassesWithDeclaredEquals(packageName);
315                assertEquals(classes);
316        }
317
318        /**
319         * Check for each class in the given package if the equals() method is
320         * implemented correct.
321         *
322         * @param packageName
323         *            the package name e.g. "patterntesting.runtime"
324         * @param excluded
325         *            classes which should be excluded from the check
326         * @see #assertEqualsOfPackage(String)
327         * @since 1.1
328         */
329        public static void assertEqualsOfPackage(final String packageName, final Class<?>... excluded) {
330                List<Class<?>> excludedList = Arrays.asList(excluded);
331        Collection<Class<? extends Object>> classes = getClassesWithDeclaredEquals(packageName);
332        LOG.debug("{} will be excluded from check.", excludedList);
333        removeClasses(classes, excludedList);
334        assertEquals(classes);
335        }
336
337        /**
338         * Check for each class in the given package if the equals() method is
339         * implemented correct. E.g. if your unit test classes ends all with
340         * "...Test" and you want to remove them from the check you can call
341         *
342         * <pre>
343         * ObjectTester.assertEqualsOfPackage("my.package", Pattern.compile(".*Test"));
344         * </pre>
345         *
346         * @param packageName
347         *            the package name e.g. "patterntesting.runtime"
348         * @param excluded
349         *            class pattern which should be excluded from the check
350         * @since 1.6
351         */
352        public static void assertEqualsOfPackage(final String packageName, final Pattern... excluded) {
353                Collection<Class<? extends Object>> classes = getClassesWithDeclaredEquals(packageName);
354                if (LOG.isDebugEnabled()) {
355                        LOG.debug("Pattern {} will be excluded from check.", Converter.toShortString(excluded));
356                }
357                removeClasses(classes, excluded);
358                assertEquals(classes);
359        }
360
361        private static Collection<Class<? extends Object>> getClassesWithDeclaredEquals(final String packageName) {
362                assert packageName != null;
363                Collection<Class<? extends Object>> concreteClasses = classpathMonitor.getConcreteClassList(packageName);
364                Collection<Class<? extends Object>> classes = new ArrayList<>(concreteClasses.size());
365                for (Class<? extends Object> clazz : concreteClasses) {
366                        if (!Modifier.isPublic(clazz.getModifiers())) {
367                                LOG.debug(clazz + " will be ignored (class is not public)");
368                                continue;
369                        }
370                        if (!hasEqualsDeclared(clazz)) {
371                                LOG.debug(clazz + " will be ignored (equals(..) not overwritten)");
372                                continue;
373                        }
374                        classes.add(clazz);
375                }
376                return classes;
377        }
378
379        /**
380         * If you want to know if a class (or one of its super classes, except
381         * object) has overwritten the equals method you can use this method here.
382         *
383         * @param clazz
384         *            the clazz
385         * @return true, if successful
386         */
387        public static boolean hasEqualsDeclared(final Class<?> clazz) {
388                try {
389                        Method method = clazz.getMethod("equals", Object.class);
390                        Class<?> declaring = method.getDeclaringClass();
391                        return !declaring.equals(Object.class);
392                } catch (SecurityException e) {
393                        LOG.info("can't get equals(..) method of " + clazz, e);
394                        return false;
395                } catch (NoSuchMethodException ex) {
396                        LOG.trace("The equals method is not overwritten:", ex);
397                        return false;
398                }
399        }
400
401        /**
402         * Check equality of the given objects by using the compareTo() method.
403         * Because casting an object to the expected Comparable is awesome we
404         * provide this additional method here
405         *
406         * @param o1
407         *            the first object (must be of type Comparable)
408         * @param o2
409         *            the second object (must be of type Comparable)
410         * @throws AssertionError
411         *             if the check fails
412         * @see ComparableTester#assertCompareTo(Comparable, Comparable)
413         * @since 1.1
414         */
415        @SuppressWarnings("unchecked")
416        public static void assertCompareTo(final Object o1, final Object o2) throws AssertionError {
417                ComparableTester.assertCompareTo((Comparable<Comparable<?>>) o1, (Comparable<Comparable<?>>) o2);
418        }
419
420        /**
421         * If a object is only partially initalized it sometimes can happen, that
422         * calling the toString() method will result in a NullPointerException. This
423         * should not happen so there are several check methods available where you
424         * can proof it.
425         *
426         * @param obj
427         *            the object to be checked
428         * @since 1.1
429         */
430        public static void assertToString(final Object obj) {
431                if (hasToStringDefaultImpl(obj)) {
432                        LOG.info(obj.getClass() + " has default implementation of toString()");
433                }
434        }
435
436        /**
437         * Normally you should overwrite the toString() method for better logging
438         * and debugging. This is the method to check it.
439         *
440         * @param obj
441         *            the object to be checked
442         * @return true, if object has default implementation
443         */
444        public static boolean hasToStringDefaultImpl(final Object obj) {
445                try {
446                        String s = obj.toString();
447                        return s.startsWith(obj.getClass().getName() + "@");
448                } catch (RuntimeException ex) {
449                        LOG.info("The toString implementation of " + obj.getClass()
450                                        + " seems to be overwritten because error happens:", ex);
451                        return false;
452                }
453        }
454
455        /**
456         * Normally you should overwrite the toString() method for better logging
457         * and debugging. This is the method to check it.
458         *
459         * @param clazz
460         *            the clazz
461         * @return true, if object has default implementation
462         */
463        public static boolean hasToStringDefaultImpl(final Class<?> clazz) {
464                Object obj = newInstanceOf(clazz);
465                return hasToStringDefaultImpl(obj);
466        }
467
468        /**
469         * Starts all known checks like checkEquals(..), checks from the
470         * SerializableTester (if the given class is serializable) or from other
471         * classes.
472         *
473         * @param <T>
474         *            the generic type
475         * @param clazz
476         *            the clazz to be checked.
477         * @since 1.1
478         */
479        @SuppressWarnings("unchecked")
480        public static <T> void assertAll(final Class<? extends T> clazz) {
481                if (LOG.isTraceEnabled()) {
482                        LOG.trace("checking all of " + clazz + "...");
483                }
484                if (hasEqualsDeclared(clazz)) {
485                        assertEquals(clazz);
486                }
487                if (hasToStringDefaultImpl(clazz)) {
488                        LOG.info(clazz + " has default implementation of toString()");
489                }
490                if (clazz.isAssignableFrom(Serializable.class)) {
491                        try {
492                                SerializableTester.assertSerialization(clazz);
493                        } catch (NotSerializableException e) {
494                                throw new AssertionError(e);
495                        }
496                }
497                if (clazz.isAssignableFrom(Cloneable.class)) {
498                        CloneableTester.assertCloning((Class<Cloneable>) clazz);
499                }
500        }
501
502        /**
503         * Check all.
504         *
505         * @param <T>
506         *            the generic type
507         * @param classes
508         *            the classes to be checked
509         * @since 1.1
510         */
511        public static <T> void assertAll(final Collection<Class<? extends T>> classes) {
512                for (Class<? extends T> clazz : classes) {
513                        assertAll(clazz);
514                }
515        }
516
517        /**
518         * Starts all known checks for all classes of the given package.
519         * <p>
520         * To get a name of a package call {@link Package#getPackage(String)}. But
521         * be sure that you can't get <em>null</em> as result. In this case use
522         * {@link #assertAllOfPackage(String)}.
523         * </p>
524         *
525         * @param pkg
526         *            the package e.g. "patterntesting.runtime"
527         * @since 1.1
528         */
529        public static void assertAll(final Package pkg) {
530                assert pkg != null;
531                assertAllOfPackage(pkg.getName());
532        }
533
534        /**
535         * Starts all known checks for all classes of the given package except for
536         * the "excluded" classes.
537         * <p>
538         * To get a name of a package call {@link Package#getPackage(String)}. But
539         * be sure that you can't get <em>null</em> as result. In this case use
540         * {@link #assertEqualsOfPackage(String, Class...)}.
541         * </p>
542         *
543         * @param pkg
544         *            the package e.g. "patterntesting.runtime"
545         * @param excluded
546         *            classes which are excluded from the check
547         * @see #assertAllOfPackage(String, Class...)
548         * @since 1.1
549         */
550        public static void assertAll(final Package pkg, final Class<?>... excluded) {
551                assert pkg != null;
552                assertAllOfPackage(pkg.getName(), excluded);
553        }
554
555        /**
556         * Starts all known checks for all classes of the given package.
557         *
558         * @param packageName
559         *            the package e.g. "patterntesting.runtime"
560         * @since 1.1
561         */
562        public static void assertAllOfPackage(final String packageName) {
563                assert packageName != null;
564                assertAllOfPackage(packageName, new Class<?>[0]);
565        }
566
567        /**
568         * Starts all known checks for all classes of the given package but not for
569         * the "excluded" classes.
570         *
571         * @param packageName
572         *            the package name e.g. "patterntesting.runtime"
573         * @param excluded
574         *            classes which should be excluded from the check
575         * @see #assertAllOfPackage(String)
576         * @since 1.1
577         */
578        public static void assertAllOfPackage(final String packageName, final Class<?>... excluded) {
579        assert packageName != null;
580                List<Class<?>> excludedList = Arrays.asList(excluded);
581        LOG.debug("{} will be excluded from check.", excludedList);
582        Collection<Class<? extends Object>> classes = classpathMonitor.getConcreteClassList(packageName);
583        classes.removeAll(excludedList);
584        removeMemberClasses(classes);
585        assertAll(classes);
586        }
587
588        /**
589         * Starts all known checks for all classes of the given package but not for
590         * the "excluded" classes. E.g. if your unit test classes ends all with
591         * "...Test" and you want to remove them from the check you can call
592         *
593         * <pre>
594         * ObjectTester.assertEqualsOfPackage(&quot;my.package&quot;, Pattern.compile(&quot;.*Test&quot;));
595         * </pre>
596         *
597         * @param packageName
598         *            the package name e.g. "patterntesting.runtime"
599         * @param excluded
600         *            class pattern which should be excluded from the check
601         * @since 1.6
602         */
603        public static void assertAllOfPackage(final String packageName, final Pattern... excluded) {
604                assert packageName != null;
605                Collection<Class<? extends Object>> classes = classpathMonitor.getConcreteClassList(packageName);
606                if (LOG.isDebugEnabled()) {
607                        LOG.debug("Pattern {} will be excluded from check.", Converter.toShortString(excluded));
608                }
609                removeClasses(classes, excluded);
610                removeMemberClasses(classes);
611                assertAll(classes);
612        }
613
614        private static void removeMemberClasses(final Collection<Class<? extends Object>> classes) {
615                Collection<Class<? extends Object>> memberClasses = new ArrayList<>();
616                for (Class<? extends Object> clazz : classes) {
617                        if (clazz.isMemberClass()) {
618                                memberClasses.add(clazz);
619                        }
620                }
621                LOG.debug("Member classes {} will be also excluded from check.", memberClasses);
622                classes.removeAll(memberClasses);
623        }
624
625        /**
626         * New instance of.
627         *
628         * @param clazz
629         *            the clazz
630         * @return the object
631         */
632        static Object newInstanceOf(final Class<?> clazz) {
633                try {
634                        return clazz.newInstance();
635                } catch (InstantiationException e) {
636                        throw new IllegalArgumentException("can't instantiate " + clazz, e);
637                } catch (IllegalAccessException e) {
638                        throw new IllegalArgumentException("can't access ctor of " + clazz, e);
639                }
640        }
641
642        /**
643         * Clone.
644         *
645         * @param orig
646         *            the orig
647         * @return the serializable
648         * @throws NotSerializableException
649         *             the not serializable exception
650         */
651        static Serializable clone(final Serializable orig) throws NotSerializableException {
652                byte[] bytes = Converter.serialize(orig);
653                try {
654                        return Converter.deserialize(bytes);
655                } catch (ClassNotFoundException canthappen) {
656                        throw new IllegalArgumentException("cannot clone " + orig, canthappen);
657                }
658        }
659
660        /**
661         * Clone.
662         *
663         * @param orig
664         *            the orig
665         * @return the object
666         */
667        static Object clone(final Object orig) {
668                if (orig instanceof Cloneable) {
669                        return CloneableTester.getCloneOf((Cloneable) orig);
670                }
671                try {
672                        return clone((Serializable) orig);
673                } catch (ClassCastException e) {
674                        LOG.trace("{} is not serializable - fallback to attribute cloning", orig.getClass(), e);
675                } catch (NotSerializableException nse) {
676                        LOG.warn("can't serialize {} - fallback to attribute cloning", orig.getClass(), nse);
677                }
678                Object clone = newInstanceOf(orig.getClass());
679                Field[] fields = orig.getClass().getDeclaredFields();
680                for (int i = 0; i < fields.length; i++) {
681                        fields[i].setAccessible(true);
682                        if (ReflectionHelper.isStatic(fields[i])) {
683                                continue;
684                        }
685                        try {
686                                Object value = fields[i].get(orig);
687                                fields[i].set(clone, value);
688                        } catch (IllegalAccessException ex) {
689                                LOG.debug(fields[i] + " is ignored:", ex);
690                        }
691                }
692                return clone;
693        }
694
695}