Skip to content

Commit

Permalink
Adds ability to reset Objenesis and detect classloader issues
Browse files Browse the repository at this point in the history
  • Loading branch information
jqno committed Dec 3, 2021
1 parent 117da77 commit ccf5ec3
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 6 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<a name="3.x"/>

## [Unreleased]
### Added
- `withResetCaches()` to reset the Objenesis caches. This can be useful if the test framework uses multiple class loaders; see for instance [this question on StackOverflow](https://stackoverflow.com/q/70123578/127863).

### Changed
- Verifies equality for `BigDecimal` using `compareTo()`. ([Issue 540](https://github.com/jqno/equalsverifier/issues/540); thanks ac183!)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import nl.jqno.equalsverifier.internal.prefabvalues.FactoryCache;
import nl.jqno.equalsverifier.internal.reflection.PackageScanner;
import nl.jqno.equalsverifier.internal.util.ListBuilders;
import nl.jqno.equalsverifier.internal.util.ObjenesisWrapper;
import nl.jqno.equalsverifier.internal.util.PrefabValuesApi;
import nl.jqno.equalsverifier.internal.util.Validations;

Expand Down Expand Up @@ -90,6 +91,13 @@ public ConfiguredEqualsVerifier usingGetClass() {
return this;
}

/** {@inheritDoc} */
@Override
public ConfiguredEqualsVerifier withResetCaches() {
ObjenesisWrapper.reset();
return this;
}

/**
* Factory method. For general use.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,13 @@ public <S> EqualsVerifierApi<T> withGenericPrefabValues(
* @see Warning#STRICT_INHERITANCE
*/
public EqualsVerifierApi<T> usingGetClass();

/**
* Signals that all internal caches need to be reset. This is useful when the test framework
* uses multiple ClassLoaders to run tests, causing {@link java.lang.reflect.Class} instances
* that would normally be equal, to be unequal, because their ClassLoaders don't match.
*
* @return {@code this}, for easy method chaining.
*/
public EqualsVerifierApi<T> withResetCaches();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import nl.jqno.equalsverifier.Warning;
import nl.jqno.equalsverifier.internal.util.Formatter;
import nl.jqno.equalsverifier.internal.util.ListBuilders;
import nl.jqno.equalsverifier.internal.util.ObjenesisWrapper;
import nl.jqno.equalsverifier.internal.util.Validations;

/**
Expand Down Expand Up @@ -68,6 +69,13 @@ public MultipleTypeEqualsVerifierApi usingGetClass() {
return this;
}

/** {@inheritDoc} */
@Override
public MultipleTypeEqualsVerifierApi withResetCaches() {
ObjenesisWrapper.reset();
return this;
}

/**
* Removes the given type or types from the list of types to verify.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,13 @@ public SingleTypeEqualsVerifierApi<T> withLombokCachedHashCode(T example) {
return this;
}

/** {@inheritDoc} */
@Override
public SingleTypeEqualsVerifierApi<T> withResetCaches() {
ObjenesisWrapper.reset();
return this;
}

/**
* Performs the verification of the contracts for {@code equals} and {@code hashCode} and throws
* an {@link AssertionError} if there is a problem.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,22 @@ private void change(FieldChanger changer, boolean includeStatic) {
}

field.setAccessible(true);
rethrow(() -> changer.change());
rethrow(() -> wrappedChange(changer));
}

private void wrappedChange(FieldChanger changer) throws IllegalAccessException {
try {
changer.change();
} catch (IllegalArgumentException e) {
if (e.getMessage().startsWith("Can not set")) {
throw new ReflectionException(
"Reflection error: perhaps a ClassLoader problem?\nTry re-running with #withResetCaches()",
e
);
} else {
throw e;
}
}
}

@FunctionalInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import org.objenesis.ObjenesisHelper;
import nl.jqno.equalsverifier.internal.util.ObjenesisWrapper;

/**
* Instantiates objects of a given class.
Expand Down Expand Up @@ -58,7 +58,7 @@ public static <T> Instantiator<T> of(Class<T> type) {
* @return An object of type T.
*/
public T instantiate() {
return ObjenesisHelper.newInstance(type);
return ObjenesisWrapper.getObjenesis().newInstance(type);
}

/**
Expand All @@ -68,7 +68,7 @@ public T instantiate() {
*/
public T instantiateAnonymousSubclass() {
Class<T> proxyClass = giveDynamicSubclass(type);
return ObjenesisHelper.newInstance(proxyClass);
return ObjenesisWrapper.getObjenesis().newInstance(proxyClass);
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package nl.jqno.equalsverifier.internal.util;

import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;

/**
* A wrapper around Objenesis. Objenesis keeps caches of objects it has instantiated, so we want a
* way to easily re-use the same instance of `Objenesis`. This class reflects the usage in
* {@link org.objenesis.ObjenesisHelper}, but with the added benefit that now we can reset the
* caches if needed (for instance if the test framework used does some "clever" tricks with
* ClassLoaders) by re-initializing the Objenesis instance.
*
* Note: I realise that a wrapper around a static reference is not very architecturally sound;
* however, doing it properly would require major re-writes. Maybe some other time.
*/
public final class ObjenesisWrapper {

private static Objenesis objenesis = new ObjenesisStd();

private ObjenesisWrapper() {}

public static Objenesis getObjenesis() {
return objenesis;
}

public static void reset() {
objenesis = new ObjenesisStd();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package nl.jqno.equalsverifier.integration.operational;

import static org.junit.jupiter.api.Assertions.assertNotEquals;

import nl.jqno.equalsverifier.EqualsVerifier;
import nl.jqno.equalsverifier.internal.util.ObjenesisWrapper;
import nl.jqno.equalsverifier.testhelpers.packages.correct.A;
import nl.jqno.equalsverifier.testhelpers.packages.correct.B;
import org.junit.jupiter.api.Test;
import org.objenesis.Objenesis;

public class WithResetCachesTest {

@Test
public void single() {
Objenesis original = ObjenesisWrapper.getObjenesis();
EqualsVerifier.forClass(A.class).withResetCaches();
Objenesis reset = ObjenesisWrapper.getObjenesis();
assertNotEquals(original, reset);
}

@Test
public void multiple() {
Objenesis original = ObjenesisWrapper.getObjenesis();
EqualsVerifier.forClasses(A.class, B.class).withResetCaches();
Objenesis reset = ObjenesisWrapper.getObjenesis();
assertNotEquals(original, reset);
}

@Test
public void configured() {
Objenesis original = ObjenesisWrapper.getObjenesis();
EqualsVerifier.configure().withResetCaches();
Objenesis reset = ObjenesisWrapper.getObjenesis();
assertNotEquals(original, reset);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
package nl.jqno.equalsverifier.internal.reflection;

import static nl.jqno.equalsverifier.internal.prefabvalues.factories.Factories.values;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.reflect.Field;
import nl.jqno.equalsverifier.internal.exceptions.ReflectionException;
import nl.jqno.equalsverifier.internal.prefabvalues.FactoryCache;
import nl.jqno.equalsverifier.internal.prefabvalues.JavaApiPrefabValues;
import nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues;
import nl.jqno.equalsverifier.internal.prefabvalues.TypeTag;
import nl.jqno.equalsverifier.testhelpers.types.Point;
import nl.jqno.equalsverifier.testhelpers.types.PointContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.*;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.AbstractAndInterfaceArrayContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.AbstractClassContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.AllArrayTypesContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.AllTypesContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.GenericListContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.GenericTypeVariableListContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.InterfaceContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.ObjectContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.Outer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.Outer.Inner;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.PointArrayContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.PrimitiveContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.PrivateObjectContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.StaticContainer;
import nl.jqno.equalsverifier.testhelpers.types.TypeHelper.StaticFinalContainer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -312,6 +333,20 @@ public void addPrefabArrayValues() {
assertEquals(RED_NEW_POINT, foo.points[0]);
}

@Test
public void shouldDetectClassloaderIssue() throws Exception {
// We're faking the problem by using two entirely different classes.
// In reality, this problem comes up with the same types, but loaded by different class loaders,
// which makes them effectively different types as well. This was hard to fake in a test.
Object foo = new ObjectContainer();
Field field = PrimitiveContainer.class.getField("field");
FieldModifier fm = FieldModifier.of(field, foo);

ReflectionException e = assertThrows(ReflectionException.class, () -> fm.set(new Object()));

assertTrue(e.getMessage().contains("perhaps a ClassLoader problem?"));
}

private void setField(Object object, String fieldName, Object value) {
getAccessorFor(object, fieldName).set(value);
}
Expand Down

0 comments on commit ccf5ec3

Please sign in to comment.