Skip to content

Commit

Permalink
Add base methods of Object to proxy object of interface
Browse files Browse the repository at this point in the history
Signed-off-by: Taeik Lim <[email protected]>
  • Loading branch information
acktsap committed Oct 22, 2024
1 parent f5dbfa0 commit 7d4935a
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public ArbitraryIntrospectorResult introspect(ArbitraryGeneratorContext context)
List<ArbitraryProperty> childrenProperties = context.getChildren();

InvocationHandlerBuilder invocationHandlerBuilder = new InvocationHandlerBuilder(
type,
new HashMap<>());

for (ArbitraryProperty arbitraryProperty : childrenProperties) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
package com.navercorp.fixturemonkey.api.introspector;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.stream.Collectors;

import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
Expand All @@ -29,9 +31,18 @@
*/
@API(since = "0.6.10", status = Status.MAINTAINED)
final class InvocationHandlerBuilder {
private static final String HASH_CODE_METHOD = "hashCode";
private static final String EQUALS_METHOD = "equals";
private static final String TO_STRING_METHOD = "toString";

private final Class<?> type;
private final Map<String, Object> generatedValuesByMethodName;

InvocationHandlerBuilder(Map<String, Object> generatedValuesByMethodName) {
InvocationHandlerBuilder(
Class<?> type,
Map<String, Object> generatedValuesByMethodName
) {
this.type = type;
this.generatedValuesByMethodName = generatedValuesByMethodName;
}

Expand All @@ -40,10 +51,63 @@ void put(String methodName, Object value) {
}

InvocationHandler build() {
return (proxy, method, args) -> generatedValuesByMethodName.get(method.getName());
return (proxy, method, args) -> {
if (HASH_CODE_METHOD.equals(method.getName()) && args == null) {
return generatedValuesByMethodName.values().hashCode();
}

if (EQUALS_METHOD.equals(method.getName()) && args.length == 1) {
Object other = args[0];
return compareAllReturnValues(other);
}

if (TO_STRING_METHOD.equals(method.getName()) && args == null) {
return toString(proxy);
}

// check no-args
if (args != null) {
return null;
}

return generatedValuesByMethodName.get(method.getName());
};
}

boolean isEmpty() {
return generatedValuesByMethodName.isEmpty();
}

private boolean compareAllReturnValues(Object other) {
if (other == null) {
return false;
}

if (!type.isInstance(other)) {
return false;
}

for (Map.Entry<String, Object> methodNameToValue : generatedValuesByMethodName.entrySet()) {
String methodName = methodNameToValue.getKey();
Object returnValue = methodNameToValue.getValue();
try {
Method otherMethod = other.getClass().getMethod(methodName);
Object otherValue = otherMethod.invoke(other);
if (!returnValue.equals(otherValue)) {
return false;
}
} catch (Exception e) {
throw new IllegalArgumentException("Unexpected error in invoking method " + methodName, e);
}
}

return true;
}

private String toString(Object proxy) {
String joined = generatedValuesByMethodName.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(", "));
return proxy.getClass().getName() + "{" + joined + "}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
package com.navercorp.fixturemonkey.api.introspector;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.stream.Collectors;

import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
Expand All @@ -30,9 +32,18 @@
@SuppressWarnings("unused")
@API(since = "0.6.10", status = Status.MAINTAINED)
final class InvocationHandlerBuilder {
private static final String HASH_CODE_METHOD = "hashCode";
private static final String EQUALS_METHOD = "equals";
private static final String TO_STRING_METHOD = "toString";

private final Class<?> type;
private final Map<String, Object> generatedValuesByMethodName;

InvocationHandlerBuilder(Map<String, Object> generatedValuesByMethodName) {
InvocationHandlerBuilder(
Class<?> type,
Map<String, Object> generatedValuesByMethodName
) {
this.type = type;
this.generatedValuesByMethodName = generatedValuesByMethodName;
}

Expand All @@ -45,11 +56,63 @@ InvocationHandler build() {
if (method.isDefault()) {
return InvocationHandler.invokeDefault(proxy, method, args);
}

if (HASH_CODE_METHOD.equals(method.getName()) && args == null) {
return generatedValuesByMethodName.values().hashCode();
}

if (EQUALS_METHOD.equals(method.getName()) && args.length == 1) {
Object other = args[0];
return compareAllReturnValues(other);
}

if (TO_STRING_METHOD.equals(method.getName()) && args == null) {
return toString(proxy);
}

// check no-args
if (args != null) {
return null;
}

return generatedValuesByMethodName.get(method.getName());
};
}

boolean isEmpty() {
return generatedValuesByMethodName.isEmpty();
}

private boolean compareAllReturnValues(Object other) {
if (other == null) {
return false;
}

if (!type.isInstance(other)) {
return false;
}

for (Map.Entry<String, Object> methodNameToValue : generatedValuesByMethodName.entrySet()) {
String methodName = methodNameToValue.getKey();
Object returnValue = methodNameToValue.getValue();
try {
Method otherMethod = other.getClass().getMethod(methodName);
Object otherValue = otherMethod.invoke(other);
if (!returnValue.equals(otherValue)) {
return false;
}
} catch (Exception e) {
throw new IllegalArgumentException("Unexpected error in invoking method " + methodName, e);
}
}

return true;
}

private String toString(Object proxy) {
String joined = generatedValuesByMethodName.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(", "));
return proxy.getClass().getName() + "{" + joined + "}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.navercorp.fixturemonkey.tests.java.AnonymousInstanceTestSpecs.InterfaceWithConstant;
import com.navercorp.fixturemonkey.tests.java.AnonymousInstanceTestSpecs.InterfaceWithParams;
import com.navercorp.fixturemonkey.tests.java.AnonymousInstanceTestSpecs.NestedInheritedInterface;
import com.navercorp.fixturemonkey.tests.java.AnonymousInstanceTestSpecs.SimilarInterface;

class AnonymousInstanceTest {
private static final FixtureMonkey SUT = FixtureMonkey.builder()
Expand All @@ -56,6 +57,29 @@ void sampleInterface() {
then(actual.integer()).isNotNull();
}

@RepeatedTest(TEST_COUNT)
void objectBaseMethods() {
Interface actual = SUT.giveMeOne(Interface.class);

then(actual.hashCode()).isNotNull();
then(actual).isEqualTo(actual);
then(actual.toString()).isNotNull();
}

@RepeatedTest(TEST_COUNT)
void equalsOnSimilarInterface() {
Interface one = SUT.giveMeBuilder(Interface.class)
.set("string", "test")
.set("integer", 123)
.sample();
SimilarInterface another = SUT.giveMeBuilder(SimilarInterface.class)
.set("string", "test")
.set("integer", 123)
.sample();

then(one).isNotEqualTo(another);
}

@RepeatedTest(TEST_COUNT)
void setInterface() {
String expected = "test";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public interface Interface {
Integer integer();
}

public interface SimilarInterface {
String string();

Integer integer();
}

public interface InterfaceWithParams {
String string(String str);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,29 @@ class AnonymousInstanceTest {
then(actual.integer()).isNotNull
}

@RepeatedTest(TEST_COUNT)
fun objectBaseMethods() {
val actual = SUT.giveMeOne<Interface>()

then(actual.hashCode()).isNotNull
then(actual).isEqualTo(actual)
then(actual.toString()).isNotNull()
}

@RepeatedTest(TEST_COUNT)
fun equalsOnSimilarInterface() {
val one = SUT.giveMeBuilder<Interface>()
.setExpGetter(Interface::string, "test")
.setExpGetter(Interface::integer, 123)
.sample()
val another = SUT.giveMeBuilder<SimilarInterface>()
.setExpGetter(SimilarInterface::string, "test")
.setExpGetter(SimilarInterface::integer, 123)
.sample()

then(one).isNotEqualTo(another)
}

@RepeatedTest(TEST_COUNT)
fun setInterface() {
val expected = "test"
Expand Down Expand Up @@ -397,6 +420,11 @@ class AnonymousInstanceTest {
fun integer(): Int
}

interface SimilarInterface {
fun string(): String
fun integer(): Int
}

interface InterfaceWithParams {
fun string(str: String): String?
fun integer(int: Int): Int?
Expand Down

0 comments on commit 7d4935a

Please sign in to comment.