patterntesting.runtime.junit.ObjectTester Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of patterntesting-rt Show documentation
Show all versions of patterntesting-rt Show documentation
PatternTesting Runtime (patterntesting-rt) is the runtime component for
the PatternTesting framework. It provides the annotations and base classes
for the PatternTesting testing framework (e.g. patterntesting-check,
patterntesting-concurrent or patterntesting-exception) but can be also
used standalone for classpath monitoring or profiling.
It uses AOP and AspectJ to perform this feat.
/*
* $Id: ObjectTester.java,v 1.34 2014/04/30 20:03:11 oboehm Exp $
*
* Copyright (c) 2010 by Oliver Boehm
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express orimplied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* (c)reated 21.07.2010 by oliver ([email protected])
*/
package patterntesting.runtime.junit;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import org.junit.Assert;
import org.slf4j.*;
import patterntesting.runtime.monitor.ClasspathMonitor;
import patterntesting.runtime.util.*;
/**
* This is a utility class to check some important methods of a class like the.
*
* {@link Object#equals(Object)} or {@link Object#hashCode()} method.
* Before v1.1 the methods are named "checkEquals" or "checkCompareTo".
* Since 1.1 these methods will have now an "assert" prefix ("assertEquals"
* or "assertCompareTo").
*
* @author oliver
* @since 1.0.3 (21.07.2010)
*/
public final class ObjectTester {
private static final Logger log = LoggerFactory.getLogger(ObjectTester.class);
private static final ClasspathMonitor classpathMonitor = ClasspathMonitor.getInstance();
/** Utility class - no need to instantiate it. */
private ObjectTester() {}
/**
* Check equality of the given objects. They must be equals otherwise an
* AssertionError will be thrown. And if A == B also
* B == A must be true (commutative law, i.e. it is a
* symmetrical operation).
*
* If two objects are equals they must have also the same hash code (but
* not the other way around). This condition is also checked here.
*
*
* Often programmers forget that the {@link Object#equals(Object)} method
* can be called with null as argument and should return
* false as result. So this case is also tested here.
*
*
* @param o1 the 1st object
* @param o2 the 2nd object
* @throws AssertionError if the check fails
* @since 1.1
*/
@SuppressWarnings("unchecked")
public static void assertEquals(final Object o1, final Object o2) throws AssertionError {
try {
Package pkg = (Package) o1;
Class> c2 = (Class>) o2;
Class>[] excluded = { c2 };
assertEquals(pkg, excluded);
} catch (ClassCastException cce) {
Assert.assertEquals(o1.getClass() + ": objects are not equals!", o1, o2);
Assert.assertEquals(o1.getClass() + ": equals not symmetrical (A == B, but B != A)", o2, o1);
Assert.assertEquals(o1.getClass() + ": objects are equals but hashCode differs!",
o1.hashCode(), o2.hashCode());
if (o1 instanceof Comparable>) {
ComparableTester.assertCompareTo((Comparable>) o1,
(Comparable>) o1);
}
assertEqualsWithNull(o1);
}
log.info("equals/hashCode implementation of " + o1.getClass() + " seems to be ok");
}
/**
* Null as argument for the equals method should always return 'false' and
* should not end with a NullPointerException.
*
* @param obj the obj
*/
private static void assertEqualsWithNull(final Object obj) {
try {
Assert.assertFalse(obj.getClass().getName() + ".equals(null) should return 'false'", obj.equals(null));
} catch (RuntimeException re) {
AssertionError error = new AssertionError(obj.getClass().getName()
+ ".equals(..) implementation does not check (correct) for null argument");
error.initCause(re);
throw error;
}
}
/**
* The given object will be serialized and deserialized to get a copy of
* that object. The copy must be equals to the original object.
*
* @param obj the object
* @throws AssertionError if the check fails
* @throws NotSerializableException if obj is not serializable
* @since 1.1
*/
public static void assertEquals(final Serializable obj)
throws AssertionError, NotSerializableException {
Object clone = clone(obj);
assertEquals(obj, clone);
}
/**
* The given object will be cloned to get a copy of that object.
* The copy must be equals to the original object.
*
* @param obj the obj
* @throws AssertionError the assertion error
* @since 1.1
*/
public static void assertEquals(final Cloneable obj) throws AssertionError {
Object clone = CloneableTester.getCloneOf(obj);
assertEquals(obj, clone);
}
/**
* This method will create two objects of the given class using the
* default constructor. So three preconditions must be true:
*
* - the class must not be abstract
* - there must be a (public) default constructor
* - it must be Cloneable, Serializable or
* return always the same object
*
*
* That a constructor creates equals objects is not true for all classes.
* For example the default constructor
* of the Date class will generate objects with different timestamps
* which are not equal. But most classes should meet the precondition.
*
* @param clazz the clazz
* @throws AssertionError if the check fails
* @since 1.1
*/
public static void assertEquals(final Class> clazz) throws AssertionError {
log.trace("checking {}.equals...", clazz);
Object o1 = newInstanceOf(clazz);
try {
assertEquals((Cloneable) o1);
} catch (ClassCastException cce) {
try {
assertEquals((Serializable) o1);
} catch (NotSerializableException nse) {
throw new AssertionError(nse);
} catch (ClassCastException e) {
Object o2 = newInstanceOf(clazz);
assertEquals(o1, o2);
}
}
}
/**
* This method will create two objects of the given class by trying to
* clone them in different ways. The last try to get a copy will be a
* cloning of all attributes. But this is normally not very significant
* because most (wrong) equals implementation depends on a simple
* comparison of the object reference.
*
* @param clazz the clazz
* @throws AssertionError if the check fails
* @since 1.1
*/
public static void assertEqualsWithClone(final Class> clazz) throws AssertionError {
if (log.isTraceEnabled()) {
log.trace("checking " + clazz.getName() + ".equals...");
}
Object o1 = newInstanceOf(clazz);
try {
assertEquals((Cloneable) o1);
} catch (ClassCastException cce) {
try {
assertEquals((Serializable) o1);
} catch (NotSerializableException nse) {
throw new AssertionError(nse);
} catch (ClassCastException e) {
Object o2 = clone(o1);
assertEquals(o1, o2);
}
}
}
/**
* Check for each class in the given collection if the equals() method
* is implemented correct.
*
* @param classes the classes
* @throws Failures the collected assertion errors
* @since 1.1
*/
public static void assertEquals(final Collection> classes) throws Failures {
Failures failures = new Failures();
for (Class> clazz : classes) {
try {
assertEquals(clazz);
} catch (AssertionError e) {
log.warn("equals/hashCode implementation of " + clazz + " is NOT OK ("
+ e.getMessage() + ")");
failures.add(clazz, e);
}
}
if (failures.hasErrors()) {
throw failures;
}
}
/**
* Check for each class in the given package if the equals() method
* is implemented correct.
*
* To get a name of a package call {@link Package#getPackage(String)}.
* But be sure that you can't get null as result. In this case
* use {@link #assertEqualsOfPackage(String)}.
*
* @param pkg the package e.g. "patterntesting.runtime"
* @see #assertEqualsOfPackage(String)
* @since 1.1
*/
public static void assertEquals(final Package pkg) {
assert pkg!= null;
assertEqualsOfPackage(pkg.getName());
}
/**
* Check for each class in the given package if the equals() method
* is implemented correct.
*
* To get a name of a package call {@link Package#getPackage(String)}.
* But be sure that you can't get null as result. In this case
* use {@link #assertEqualsOfPackage(String, Class...)}.
*
* @param pkg the package e.g. "patterntesting.runtime"
* @param excluded classes which are excluded from the check
* @see #assertEqualsOfPackage(String, Class...)
* @since 1.1
*/
public static void assertEquals(final Package pkg, final Class>... excluded) {
assert pkg!= null;
assertEqualsOfPackage(pkg.getName(), excluded);
}
/**
* Check for each class in the given package if the equals() method
* is implemented correct.
*
* To get a name of a package call {@link Package#getPackage(String)}.
* But be sure that you can't get null as result. In this case
* use {@link #assertEqualsOfPackage(String, List)}.
*
* @param pkg the package e.g. "patterntesting.runtime"
* @param excluded classes which should be excluded from the check
* @see #assertEqualsOfPackage(String, List)
* @since 1.1
*/
public static void assertEqualsOfPackage(final Package pkg, final List> excluded) {
assert pkg!= null;
assertEqualsOfPackage(pkg.getName(), excluded);
}
/**
* Check for each class in the given package if the equals() method
* is implemented correct.
*
* This method does the same as {@link #assertEquals(Package)} but was
* introduced by {@link Package#getPackage(String)} sometimes return null
* if no class of this package is loaded.
*
* @param packageName the package name e.g. "patterntesting.runtime"
* @see #assertEquals(Package)
* @since 1.1
*/
public static void assertEqualsOfPackage(final String packageName) {
Collection> classes = getClassesWithDeclaredEquals(packageName);
assertEquals(classes);
}
/**
* Check for each class in the given package if the equals() method
* is implemented correct.
*
* @param packageName the package name e.g. "patterntesting.runtime"
* @param excluded classes which should be excluded from the check
* @see #assertEqualsOfPackage(String)
* @since 1.1
*/
public static void assertEqualsOfPackage(final String packageName, final Class>... excluded) {
List> excludedList = Arrays.asList(excluded);
assertEqualsOfPackage(packageName, excludedList);
}
/**
* Check for each class in the given package if the equals() method
* is implemented correct.
*
* @param packageName the package name e.g. "patterntesting.runtime"
* @param excluded classes which should be excluded from the check
* @see #assertEqualsOfPackage(String)
* @since 1.1
*/
public static void assertEqualsOfPackage(final String packageName, final List> excluded) {
Collection> classes = getClassesWithDeclaredEquals(packageName);
log.debug(excluded + " will be excluded from check");
removeClasses(classes, excluded);
assertEquals(classes);
}
private static Collection> getClassesWithDeclaredEquals(final String packageName) {
assert packageName != null;
Collection> concreteClasses = classpathMonitor.getConcreteClassList(packageName);
Collection> classes = new ArrayList>(concreteClasses.size());
for (Class> clazz : concreteClasses) {
if (!hasEqualsDeclared(clazz)) {
log.debug(clazz + " will be ignored (equals(..) not overwritten)");
continue;
}
classes.add(clazz);
}
return classes;
}
/**
* If you want to know if a class (or one of its super classes, except
* object) has overwritten the equals method you
* can use this method here.
*
* @param clazz the clazz
* @return true, if successful
*/
public static boolean hasEqualsDeclared(final Class> clazz) {
try {
Method method = clazz.getMethod("equals", Object.class);
Class> declaring = method.getDeclaringClass();
return !declaring.equals(Object.class);
} catch (SecurityException e) {
log.info("can't get equals(..) method of " + clazz, e);
return false;
} catch (NoSuchMethodException e) {
return false;
}
}
/**
* Check equality of the given objects by using the compareTo() method.
* Because casting an object to the expected Comparable is awesome we
* provide this additional method here
*
* @param o1 the first object (must be of type Comparable)
* @param o2 the second object (must be of type Comparable)
* @throws AssertionError if the check fails
* @see ComparableTester#assertCompareTo(Comparable, Comparable)
* @since 1.1
*/
@SuppressWarnings("unchecked")
public static void assertCompareTo(final Object o1, final Object o2) throws AssertionError {
ComparableTester.assertCompareTo((Comparable>) o1,
(Comparable>) o2);
}
/**
* If a object is only partially initalized it sometimes can happen, that
* calling the toString() method will result in a NullPointerException.
* This should not happen so there are several check methods available
* where you can proof it.
*
* @param obj the object to be checked
* @since 1.1
*/
public static void assertToString(final Object obj) {
if (hasToStringDefaultImpl(obj)) {
log.info(obj.getClass() + " has default implementation of toString()");
}
}
/**
* Normally you should overwrite the toString() method for better logging
* and debugging. This is the method to check it.
*
* @param obj the object to be checked
* @return true, if object has default implementation
*/
public static boolean hasToStringDefaultImpl(final Object obj) {
try {
String s = obj.toString();
return s.startsWith(obj.getClass().getName() + "@");
} catch (Throwable t) {
log.info("The toString implementation of " + obj.getClass()
+ " seems to be overwritten because error happens.", t);
return false;
}
}
/**
* Normally you should overwrite the toString() method for better logging
* and debugging. This is the method to check it.
*
* @param clazz the clazz
* @return true, if object has default implementation
*/
public static boolean hasToStringDefaultImpl(final Class> clazz) {
Object obj = newInstanceOf(clazz);
return hasToStringDefaultImpl(obj);
}
/**
* Starts all known checks like checkEquals(..), checks from the
* SerializableTester (if the given class is serializable) or from
* other classes.
*
* @param clazz the clazz to be checked.
* @since 1.1
*/
@SuppressWarnings("unchecked")
public static void assertAll(final Class> clazz) {
if (log.isTraceEnabled()) {
log.trace("checking all of " + clazz + "...");
}
if (hasEqualsDeclared(clazz)) {
assertEquals(clazz);
}
if (hasToStringDefaultImpl(clazz)) {
log.info(clazz + " has default implementation of toString()");
}
if (clazz.isAssignableFrom(Serializable.class)) {
try {
SerializableTester.assertSerialization(clazz);
} catch (NotSerializableException e) {
throw new AssertionError(e);
}
}
if (clazz.isAssignableFrom(Cloneable.class)) {
CloneableTester.assertCloning((Class) clazz);
}
}
/**
* Check all.
*
* @param classes the classes to be checked
* @since 1.1
*/
public static void assertAll(final Collection> classes) {
for (Class> clazz : classes) {
assertAll(clazz);
}
}
/**
* Starts all known checks for all classes of the given package.
*
* To get a name of a package call {@link Package#getPackage(String)}.
* But be sure that you can't get null as result. In this case
* use {@link #assertAllOfPackage(String)}.
*
* @param pkg the package e.g. "patterntesting.runtime"
* @since 1.1
*/
public static void assertAll(final Package pkg) {
assert pkg != null;
assertAllOfPackage(pkg.getName());
}
/**
* Starts all known checks for all classes of the given package except for
* the "excluded" classes.
*
* To get a name of a package call {@link Package#getPackage(String)}.
* But be sure that you can't get null as result. In this case
* use {@link #assertEqualsOfPackage(String, Class...)}.
*
* @param pkg the package e.g. "patterntesting.runtime"
* @param excluded classes which are excluded from the check
* @see #assertAllOfPackage(String, Class...)
* @since 1.1
*/
public static void assertAll(final Package pkg, final Class>... excluded) {
assert pkg!= null;
assertAllOfPackage(pkg.getName(), excluded);
}
/**
* Starts all known checks for all classes of the given package.
*
* @param packageName the package e.g. "patterntesting.runtime"
* @since 1.1
*/
public static void assertAllOfPackage(final String packageName) {
assert packageName != null;
assertAllOfPackage(packageName, new ArrayList>());
}
/**
* Starts all known checks for all classes of the given package but not
* for the "excluded" classes.
*
* @param packageName the package name e.g. "patterntesting.runtime"
* @param excluded classes which should be excluded from the check
* @see #assertAllOfPackage(String)
* @since 1.1
*/
public static void assertAllOfPackage(final String packageName, final Class>... excluded) {
List> excludedList = Arrays.asList(excluded);
assertAllOfPackage(packageName, excludedList);
}
/**
* Starts all known checks for all classes of the given package but not
* for the "excluded" classes.
*
* @param packageName the package name e.g. "patterntesting.runtime"
* @param excluded classes which should be excluded from the check
* @see #assertEqualsOfPackage(String)
* @since 1.1
*/
public static void assertAllOfPackage(final String packageName, final List> excluded) {
assert packageName != null;
Collection> classes = classpathMonitor.getConcreteClassList(packageName);
log.debug("{} will be excluded from check", excluded);
classes.removeAll(excluded);
Collection> memberClasses = new ArrayList>();
for (Class> clazz : classes) {
if (clazz.isMemberClass()) {
memberClasses.add(clazz);
}
}
log.debug("{} will be also excluded from check (member class)", memberClasses);
classes.removeAll(memberClasses);
assertAll(classes);
}
/**
* New instance of.
*
* @param clazz the clazz
* @return the object
*/
static Object newInstanceOf(final Class> clazz) {
try {
return clazz.newInstance();
} catch (InstantiationException e) {
throw new IllegalArgumentException("can't instantiate " + clazz, e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("can't access ctor of " + clazz, e);
}
}
/**
* Clone.
*
* @param orig the orig
* @return the serializable
* @throws NotSerializableException the not serializable exception
*/
static Serializable clone(final Serializable orig) throws NotSerializableException {
byte[] bytes = Converter.serialize(orig);
try {
return Converter.deserialize(bytes);
} catch (ClassNotFoundException canthappen) {
throw new RuntimeException("ups, something strange happened",
canthappen);
}
}
/**
* Clone.
*
* @param orig the orig
* @return the object
*/
static Object clone(final Object orig) {
if (orig instanceof Cloneable) {
return CloneableTester.getCloneOf((Cloneable) orig);
}
try {
return clone((Serializable) orig);
} catch (ClassCastException e) {
log.trace("{} is not serializable - fallback to attribute cloning", orig.getClass(), e);
} catch (NotSerializableException nse) {
log.warn("can't serialize {} - fallback to attribute cloning", orig.getClass(), nse);
}
Object clone = newInstanceOf(orig.getClass());
Field[] fields = orig.getClass().getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
if (ReflectionHelper.isStatic(fields[i])) {
continue;
}
try {
Object value = fields[i].get(orig);
fields[i].set(clone, value);
} catch (IllegalAccessException e) {
log.debug("ignore " + fields[i] + " (" + e.getMessage() + ")");
}
}
return clone;
}
/**
* Removes "excluded" from the given classes. If one of the "excluded"
* class is an interface or abstract class all implementing or subclasses
* will be excluded.
*
* @param classes the classes
* @param excluded the excluded
*/
static void removeClasses(final Collection> classes, final List> excluded) {
classes.removeAll(excluded);
for (Object obj : excluded) {
Class> clazz = (Class>) obj;
if (clazz.isInterface() || isAbstract(clazz)) {
removeAssignableClasses(classes, clazz);
}
}
}
private static boolean isAbstract(final Class> clazz) {
return Modifier.isAbstract(clazz.getModifiers());
}
private static void removeAssignableClasses(final Collection> classes, final Class> superclass) {
Collection> toBeDeleted = new ArrayList>();
for (Object obj : classes) {
Class> clazz = (Class>) obj;
if (superclass.isAssignableFrom(clazz)) {
if (log.isTraceEnabled()) {
log.trace("removing " + clazz + " from list of classes...");
}
toBeDeleted.add(clazz);
}
}
classes.removeAll(toBeDeleted);
}
}